diff options
| author | Jonatan Rhodin <jonatan.rhodin@mullvad.net> | 2025-07-30 14:18:10 +0200 |
|---|---|---|
| committer | Jonatan Rhodin <jonatan.rhodin@mullvad.net> | 2025-07-30 14:18:10 +0200 |
| commit | 83a65f5b2568543a5ff09bc90c4fca43ae2d3b0c (patch) | |
| tree | 426e1981e717f1129dfaf7d01d85e32e0e190440 /android | |
| parent | 155fa3bc298babfac523e621427d8ca9ac442b97 (diff) | |
| parent | 4d4677a8fc7ed2339d8c744f8bd93b48e3b90c29 (diff) | |
| download | mullvadvpn-83a65f5b2568543a5ff09bc90c4fca43ae2d3b0c.tar.xz mullvadvpn-83a65f5b2568543a5ff09bc90c4fca43ae2d3b0c.zip | |
Merge branch 'migrate-settings-screens-and-viewmodels-to-new-lce-structure-droid-2088'
Diffstat (limited to 'android')
49 files changed, 1267 insertions, 932 deletions
diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ServerIpOverridesScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ServerIpOverridesScreenTest.kt index cf703c26e5..6002ce6044 100644 --- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ServerIpOverridesScreenTest.kt +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ServerIpOverridesScreenTest.kt @@ -16,6 +16,8 @@ import net.mullvad.mullvadvpn.lib.ui.tag.SERVER_IP_OVERRIDE_IMPORT_TEST_TAG import net.mullvad.mullvadvpn.lib.ui.tag.SERVER_IP_OVERRIDE_INFO_TEST_TAG import net.mullvad.mullvadvpn.lib.ui.tag.SERVER_IP_OVERRIDE_MORE_VERT_TEST_TAG import net.mullvad.mullvadvpn.lib.ui.tag.SERVER_IP_OVERRIDE_RESET_OVERRIDES_TEST_TAG +import net.mullvad.mullvadvpn.util.Lc +import net.mullvad.mullvadvpn.util.toLc import net.mullvad.mullvadvpn.viewmodel.ServerIpOverridesUiState import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -31,7 +33,7 @@ class ServerIpOverridesScreenTest { } private fun ComposeContext.initScreen( - state: ServerIpOverridesUiState, + state: Lc<Boolean, ServerIpOverridesUiState>, onBackClick: () -> Unit = {}, onInfoClick: () -> Unit = {}, onResetOverridesClick: () -> Unit = {}, @@ -54,7 +56,7 @@ class ServerIpOverridesScreenTest { fun ensureOverridesInactiveIsDisplayed() = composeExtension.use { // Arrange - initScreen(state = ServerIpOverridesUiState.Loaded(false)) + initScreen(state = ServerIpOverridesUiState(false).toLc()) // Assert onNodeWithText("Overrides inactive").assertExists() @@ -64,7 +66,7 @@ class ServerIpOverridesScreenTest { fun ensureOverridesActiveIsDisplayed() = composeExtension.use { // Arrange - initScreen(state = ServerIpOverridesUiState.Loaded(true)) + initScreen(state = ServerIpOverridesUiState(true).toLc()) // Assert onNodeWithText("Overrides active").assertExists() @@ -74,7 +76,7 @@ class ServerIpOverridesScreenTest { fun ensureOverridesActiveShowsWarningOnImport() = composeExtension.use { // Arrange - initScreen(state = ServerIpOverridesUiState.Loaded(true)) + initScreen(state = ServerIpOverridesUiState(true).toLc()) // Act onNodeWithTag(testTag = SERVER_IP_OVERRIDE_IMPORT_TEST_TAG).performClick() @@ -91,7 +93,7 @@ class ServerIpOverridesScreenTest { composeExtension.use { // Arrange val clickHandler: () -> Unit = mockk(relaxed = true) - initScreen(state = ServerIpOverridesUiState.Loaded(false), onInfoClick = clickHandler) + initScreen(state = ServerIpOverridesUiState(false).toLc(), onInfoClick = clickHandler) // Act onNodeWithTag(SERVER_IP_OVERRIDE_INFO_TEST_TAG).performClick() @@ -106,7 +108,7 @@ class ServerIpOverridesScreenTest { // Arrange val clickHandler: () -> Unit = mockk(relaxed = true) initScreen( - state = ServerIpOverridesUiState.Loaded(true), + state = ServerIpOverridesUiState(true).toLc(), onResetOverridesClick = clickHandler, ) @@ -124,7 +126,7 @@ class ServerIpOverridesScreenTest { // Arrange val clickHandler: () -> Unit = mockk(relaxed = true) initScreen( - state = ServerIpOverridesUiState.Loaded(false), + state = ServerIpOverridesUiState(false).toLc(), onImportByFile = clickHandler, ) @@ -142,7 +144,7 @@ class ServerIpOverridesScreenTest { // Arrange val clickHandler: () -> Unit = mockk(relaxed = true) initScreen( - state = ServerIpOverridesUiState.Loaded(false), + state = ServerIpOverridesUiState(false).toLc(), onImportByText = clickHandler, ) diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/SettingsScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/SettingsScreenTest.kt index b343c44c95..45967cd7fd 100644 --- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/SettingsScreenTest.kt +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/SettingsScreenTest.kt @@ -8,6 +8,8 @@ import io.mockk.MockKAnnotations import net.mullvad.mullvadvpn.compose.createEdgeToEdgeComposeExtension import net.mullvad.mullvadvpn.compose.setContentWithTheme import net.mullvad.mullvadvpn.compose.state.SettingsUiState +import net.mullvad.mullvadvpn.util.Lc +import net.mullvad.mullvadvpn.util.toLc import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension @@ -22,7 +24,7 @@ class SettingsScreenTest { } private fun ComposeContext.initScreen( - state: SettingsUiState, + state: Lc<Unit, SettingsUiState>, onVpnSettingCellClick: () -> Unit = {}, onSplitTunnelingCellClick: () -> Unit = {}, onAppInfoClick: () -> Unit = {}, @@ -55,13 +57,14 @@ class SettingsScreenTest { initScreen( state = SettingsUiState( - appVersion = "", - isLoggedIn = true, - isSupportedVersion = true, - isPlayBuild = false, - multihopEnabled = false, - isDaitaEnabled = false, - ) + appVersion = "", + isLoggedIn = true, + isSupportedVersion = true, + isPlayBuild = false, + multihopEnabled = false, + isDaitaEnabled = false, + ) + .toLc() ) // Assert onNodeWithText("VPN settings").assertExists() @@ -78,13 +81,14 @@ class SettingsScreenTest { initScreen( state = SettingsUiState( - appVersion = "", - isLoggedIn = false, - isSupportedVersion = true, - isPlayBuild = false, - multihopEnabled = false, - isDaitaEnabled = false, - ) + appVersion = "", + isLoggedIn = false, + isSupportedVersion = true, + isPlayBuild = false, + multihopEnabled = false, + isDaitaEnabled = false, + ) + .toLc() ) // Assert onNodeWithText("VPN settings").assertDoesNotExist() diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ShadowsocksSettingsScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ShadowsocksSettingsScreenTest.kt index e186e93319..0a434875df 100644 --- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ShadowsocksSettingsScreenTest.kt +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ShadowsocksSettingsScreenTest.kt @@ -9,10 +9,12 @@ import io.mockk.mockk import io.mockk.verify import net.mullvad.mullvadvpn.compose.createEdgeToEdgeComposeExtension import net.mullvad.mullvadvpn.compose.setContentWithTheme -import net.mullvad.mullvadvpn.compose.state.ShadowsocksSettingsState +import net.mullvad.mullvadvpn.compose.state.ShadowsocksSettingsUiState import net.mullvad.mullvadvpn.lib.model.Constraint import net.mullvad.mullvadvpn.lib.model.Port import net.mullvad.mullvadvpn.lib.ui.tag.SHADOWSOCKS_CUSTOM_PORT_TEXT_TEST_TAG +import net.mullvad.mullvadvpn.util.Lc +import net.mullvad.mullvadvpn.util.toLc import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension @@ -21,8 +23,8 @@ class ShadowsocksSettingsScreenTest { @JvmField @RegisterExtension val composeExtension = createEdgeToEdgeComposeExtension() private fun ComposeContext.initScreen( - state: ShadowsocksSettingsState = ShadowsocksSettingsState(), - navigateToCustomPortDialog: () -> Unit = {}, + state: Lc<Unit, ShadowsocksSettingsUiState>, + navigateToCustomPortDialog: (port: Port?) -> Unit = {}, onObfuscationPortSelected: (Constraint<Port>) -> Unit = {}, onBackClick: () -> Unit = {}, ) { @@ -40,7 +42,7 @@ class ShadowsocksSettingsScreenTest { fun testShowShadowsocksCustomPort() = composeExtension.use { // Arrange - initScreen(state = ShadowsocksSettingsState(customPort = Port(4000))) + initScreen(state = ShadowsocksSettingsUiState(customPort = Port(4000)).toLc()) // Assert onNodeWithText("4000").assertExists() @@ -53,10 +55,11 @@ class ShadowsocksSettingsScreenTest { val onObfuscationPortSelected: (Constraint<Port>) -> Unit = mockk(relaxed = true) initScreen( state = - ShadowsocksSettingsState( - port = Constraint.Only(Port(4000)), - customPort = Port(4000), - ), + ShadowsocksSettingsUiState( + port = Constraint.Only(Port(4000)), + customPort = Port(4000), + ) + .toLc(), onObfuscationPortSelected = onObfuscationPortSelected, ) diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/SplitTunnelingScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/SplitTunnelingScreenTest.kt index c17cb9079b..c3c50dea0f 100644 --- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/SplitTunnelingScreenTest.kt +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/SplitTunnelingScreenTest.kt @@ -12,7 +12,10 @@ import io.mockk.verify import net.mullvad.mullvadvpn.applist.AppData import net.mullvad.mullvadvpn.compose.createEdgeToEdgeComposeExtension import net.mullvad.mullvadvpn.compose.setContentWithTheme -import net.mullvad.mullvadvpn.compose.state.SplitTunnelingUiState +import net.mullvad.mullvadvpn.util.Lc +import net.mullvad.mullvadvpn.util.toLc +import net.mullvad.mullvadvpn.viewmodel.Loading +import net.mullvad.mullvadvpn.viewmodel.SplitTunnelingUiState import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -33,7 +36,7 @@ class SplitTunnelingScreenTest { } private fun ComposeContext.initScreen( - state: SplitTunnelingUiState, + state: Lc<Loading, SplitTunnelingUiState>, onEnableSplitTunneling: (Boolean) -> Unit = {}, onShowSystemAppsClick: (show: Boolean) -> Unit = {}, onExcludeAppClick: (packageName: String) -> Unit = {}, @@ -58,7 +61,7 @@ class SplitTunnelingScreenTest { fun testLoadingState() = composeExtension.use { // Arrange - initScreen(state = SplitTunnelingUiState.Loading(enabled = true)) + initScreen(state = Lc.Loading(Loading(enabled = true))) // Assert onNodeWithText(TITLE).assertExists() @@ -86,12 +89,13 @@ class SplitTunnelingScreenTest { ) initScreen( state = - SplitTunnelingUiState.ShowAppList( - enabled = true, - excludedApps = listOf(excludedApp), - includedApps = listOf(includedApp), - showSystemApps = false, - ) + SplitTunnelingUiState( + enabled = true, + excludedApps = listOf(excludedApp), + includedApps = listOf(includedApp), + showSystemApps = false, + ) + .toLc() ) // Assert @@ -116,12 +120,13 @@ class SplitTunnelingScreenTest { ) initScreen( state = - SplitTunnelingUiState.ShowAppList( - enabled = true, - excludedApps = emptyList(), - includedApps = listOf(includedApp), - showSystemApps = false, - ) + SplitTunnelingUiState( + enabled = true, + excludedApps = emptyList(), + includedApps = listOf(includedApp), + showSystemApps = false, + ) + .toLc() ) // Assert @@ -153,12 +158,13 @@ class SplitTunnelingScreenTest { val mockedClickHandler: (String) -> Unit = mockk(relaxed = true) initScreen( state = - SplitTunnelingUiState.ShowAppList( - enabled = true, - excludedApps = listOf(excludedApp), - includedApps = listOf(includedApp), - showSystemApps = false, - ), + SplitTunnelingUiState( + enabled = true, + excludedApps = listOf(excludedApp), + includedApps = listOf(includedApp), + showSystemApps = false, + ) + .toLc(), onExcludeAppClick = mockedClickHandler, ) @@ -188,12 +194,13 @@ class SplitTunnelingScreenTest { val mockedClickHandler: (String) -> Unit = mockk(relaxed = true) initScreen( state = - SplitTunnelingUiState.ShowAppList( - enabled = true, - excludedApps = listOf(excludedApp), - includedApps = listOf(includedApp), - showSystemApps = false, - ), + SplitTunnelingUiState( + enabled = true, + excludedApps = listOf(excludedApp), + includedApps = listOf(includedApp), + showSystemApps = false, + ) + .toLc(), onIncludeAppClick = mockedClickHandler, ) @@ -223,12 +230,13 @@ class SplitTunnelingScreenTest { val mockedClickHandler: (Boolean) -> Unit = mockk(relaxed = true) initScreen( state = - SplitTunnelingUiState.ShowAppList( - enabled = true, - excludedApps = listOf(excludedApp), - includedApps = listOf(includedApp), - showSystemApps = false, - ), + SplitTunnelingUiState( + enabled = true, + excludedApps = listOf(excludedApp), + includedApps = listOf(includedApp), + showSystemApps = false, + ) + .toLc(), onShowSystemAppsClick = mockedClickHandler, ) diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/Udp2TcpSettingsScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/Udp2TcpSettingsScreenTest.kt index b128a5054a..04035cd667 100644 --- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/Udp2TcpSettingsScreenTest.kt +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/Udp2TcpSettingsScreenTest.kt @@ -7,11 +7,13 @@ import io.mockk.coVerify import io.mockk.mockk import net.mullvad.mullvadvpn.compose.createEdgeToEdgeComposeExtension import net.mullvad.mullvadvpn.compose.setContentWithTheme -import net.mullvad.mullvadvpn.compose.state.Udp2TcpSettingsState +import net.mullvad.mullvadvpn.compose.state.Udp2TcpSettingsUiState import net.mullvad.mullvadvpn.lib.model.Constraint import net.mullvad.mullvadvpn.lib.model.Port import net.mullvad.mullvadvpn.lib.ui.tag.UDP_OVER_TCP_PORT_ITEM_X_TEST_TAG import net.mullvad.mullvadvpn.onNodeWithTagAndText +import net.mullvad.mullvadvpn.util.Lc +import net.mullvad.mullvadvpn.util.toLc import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension @@ -20,7 +22,7 @@ class Udp2TcpSettingsScreenTest { @JvmField @RegisterExtension val composeExtension = createEdgeToEdgeComposeExtension() private fun ComposeContext.initScreen( - state: Udp2TcpSettingsState = Udp2TcpSettingsState(), + state: Lc<Unit, Udp2TcpSettingsUiState>, onObfuscationPortSelected: (Constraint<Port>) -> Unit = {}, navigateUdp2TcpInfo: () -> Unit = {}, onBackClick: () -> Unit = {}, @@ -41,7 +43,7 @@ class Udp2TcpSettingsScreenTest { // Arrange val onObfuscationPortSelected: (Constraint<Port>) -> Unit = mockk(relaxed = true) initScreen( - state = Udp2TcpSettingsState(port = Constraint.Any), + state = Udp2TcpSettingsUiState(port = Constraint.Any).toLc(), onObfuscationPortSelected = onObfuscationPortSelected, ) diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreenTest.kt index f2409aef44..310ebcdc6f 100644 --- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreenTest.kt +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreenTest.kt @@ -13,6 +13,8 @@ import io.mockk.mockk import io.mockk.verify import net.mullvad.mullvadvpn.compose.createEdgeToEdgeComposeExtension import net.mullvad.mullvadvpn.compose.setContentWithTheme +import net.mullvad.mullvadvpn.compose.state.CustomDnsItem +import net.mullvad.mullvadvpn.compose.state.VpnSettingsUiState import net.mullvad.mullvadvpn.lib.model.Constraint import net.mullvad.mullvadvpn.lib.model.DefaultDnsOptions import net.mullvad.mullvadvpn.lib.model.IpVersion @@ -30,8 +32,8 @@ import net.mullvad.mullvadvpn.lib.ui.tag.LAZY_LIST_WIREGUARD_CUSTOM_PORT_TEXT_TE import net.mullvad.mullvadvpn.lib.ui.tag.LAZY_LIST_WIREGUARD_OBFUSCATION_TITLE_TEST_TAG import net.mullvad.mullvadvpn.lib.ui.tag.LAZY_LIST_WIREGUARD_PORT_ITEM_X_TEST_TAG import net.mullvad.mullvadvpn.onNodeWithTagAndText -import net.mullvad.mullvadvpn.viewmodel.CustomDnsItem -import net.mullvad.mullvadvpn.viewmodel.VpnSettingsUiState +import net.mullvad.mullvadvpn.util.Lc +import net.mullvad.mullvadvpn.util.toLc import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension @@ -65,7 +67,7 @@ class VpnSettingsScreenTest { isContentBlockersExpanded: Boolean = false, isModal: Boolean = false, ) = - VpnSettingsUiState.Content.from( + VpnSettingsUiState.from( mtu = mtu, isLocalNetworkSharingEnabled = isLocalNetworkSharingEnabled, isCustomDnsEnabled = isCustomDnsEnabled, @@ -87,7 +89,7 @@ class VpnSettingsScreenTest { ) private fun ComposeContext.initScreen( - state: VpnSettingsUiState = createDefaultUiState(), + state: Lc<Boolean, VpnSettingsUiState> = createDefaultUiState().toLc(), navigateToContentBlockersInfo: () -> Unit = {}, navigateToAutoConnectScreen: () -> Unit = {}, navigateToCustomDnsInfo: () -> Unit = {}, @@ -184,6 +186,7 @@ class VpnSettingsScreenTest { initScreen( state = createDefaultUiState(mtu = Mtu.fromString(VALID_DUMMY_MTU_VALUE).getOrNull()!!) + .toLc() ) onNodeWithTag(LAZY_LIST_VPN_SETTINGS_TEST_TAG) @@ -200,14 +203,15 @@ class VpnSettingsScreenTest { initScreen( state = createDefaultUiState( - isCustomDnsEnabled = true, - customDnsItems = - listOf( - CustomDnsItem(address = DUMMY_DNS_ADDRESS, false, false), - CustomDnsItem(address = DUMMY_DNS_ADDRESS_2, false, false), - CustomDnsItem(address = DUMMY_DNS_ADDRESS_3, false, false), - ), - ) + isCustomDnsEnabled = true, + customDnsItems = + listOf( + CustomDnsItem(address = DUMMY_DNS_ADDRESS, false, false), + CustomDnsItem(address = DUMMY_DNS_ADDRESS_2, false, false), + CustomDnsItem(address = DUMMY_DNS_ADDRESS_3, false, false), + ), + ) + .toLc() ) // Assert @@ -224,10 +228,11 @@ class VpnSettingsScreenTest { initScreen( state = createDefaultUiState( - isCustomDnsEnabled = false, - customDnsItems = - listOf(CustomDnsItem(address = DUMMY_DNS_ADDRESS, false, false)), - ) + isCustomDnsEnabled = false, + customDnsItems = + listOf(CustomDnsItem(address = DUMMY_DNS_ADDRESS, false, false)), + ) + .toLc() ) onNodeWithTag(LAZY_LIST_VPN_SETTINGS_TEST_TAG) .performScrollToNode(hasTestTag(LAZY_LIST_LAST_ITEM_TEST_TAG)) @@ -243,17 +248,18 @@ class VpnSettingsScreenTest { initScreen( state = createDefaultUiState( - isCustomDnsEnabled = true, - isLocalNetworkSharingEnabled = true, - customDnsItems = - listOf( - CustomDnsItem( - address = DUMMY_DNS_ADDRESS, - isLocal = true, - isIpv6 = false, - ) - ), - ) + isCustomDnsEnabled = true, + isLocalNetworkSharingEnabled = true, + customDnsItems = + listOf( + CustomDnsItem( + address = DUMMY_DNS_ADDRESS, + isLocal = true, + isIpv6 = false, + ) + ), + ) + .toLc() ) // Assert @@ -267,16 +273,17 @@ class VpnSettingsScreenTest { initScreen( state = createDefaultUiState( - isCustomDnsEnabled = true, - customDnsItems = - listOf( - CustomDnsItem( - address = DUMMY_DNS_ADDRESS, - isLocal = false, - isIpv6 = false, - ) - ), - ) + isCustomDnsEnabled = true, + customDnsItems = + listOf( + CustomDnsItem( + address = DUMMY_DNS_ADDRESS, + isLocal = false, + isIpv6 = false, + ) + ), + ) + .toLc() ) // Assert @@ -290,16 +297,17 @@ class VpnSettingsScreenTest { initScreen( state = createDefaultUiState( - isCustomDnsEnabled = true, - customDnsItems = - listOf( - CustomDnsItem( - address = DUMMY_DNS_ADDRESS, - isLocal = false, - isIpv6 = false, - ) - ), - ) + isCustomDnsEnabled = true, + customDnsItems = + listOf( + CustomDnsItem( + address = DUMMY_DNS_ADDRESS, + isLocal = false, + isIpv6 = false, + ) + ), + ) + .toLc() ) // Assert @@ -313,16 +321,17 @@ class VpnSettingsScreenTest { initScreen( state = createDefaultUiState( - isCustomDnsEnabled = true, - customDnsItems = - listOf( - CustomDnsItem( - address = DUMMY_DNS_ADDRESS, - isLocal = true, - isIpv6 = false, - ) - ), - ) + isCustomDnsEnabled = true, + customDnsItems = + listOf( + CustomDnsItem( + address = DUMMY_DNS_ADDRESS, + isLocal = true, + isIpv6 = false, + ) + ), + ) + .toLc() ) // Assert @@ -333,7 +342,9 @@ class VpnSettingsScreenTest { fun testShowSelectedTunnelQuantumOption() = composeExtension.use { // Arrange - initScreen(state = createDefaultUiState(quantumResistant = QuantumResistantState.On)) + initScreen( + state = createDefaultUiState(quantumResistant = QuantumResistantState.On).toLc() + ) onNodeWithTag(LAZY_LIST_VPN_SETTINGS_TEST_TAG) .performScrollToNode(hasTestTag(LAZY_LIST_QUANTUM_ITEM_OFF_TEST_TAG)) @@ -349,7 +360,7 @@ class VpnSettingsScreenTest { val mockSelectQuantumResistantSettingListener: (QuantumResistantState) -> Unit = mockk(relaxed = true) initScreen( - state = createDefaultUiState(quantumResistant = QuantumResistantState.Auto), + state = createDefaultUiState(quantumResistant = QuantumResistantState.Auto).toLc(), onSelectQuantumResistanceSetting = mockSelectQuantumResistantSettingListener, ) onNodeWithTag(LAZY_LIST_VPN_SETTINGS_TEST_TAG) @@ -368,7 +379,8 @@ class VpnSettingsScreenTest { composeExtension.use { // Arrange initScreen( - state = createDefaultUiState(selectedWireguardPort = Constraint.Only(Port(53))) + state = + createDefaultUiState(selectedWireguardPort = Constraint.Only(Port(53))).toLc() ) // Act @@ -392,7 +404,8 @@ class VpnSettingsScreenTest { val mockSelectWireguardPortSelectionListener: (Constraint<Port>) -> Unit = mockk(relaxed = true) initScreen( - state = createDefaultUiState(selectedWireguardPort = Constraint.Only(Port(53))), + state = + createDefaultUiState(selectedWireguardPort = Constraint.Only(Port(53))).toLc(), onWireguardPortSelected = mockSelectWireguardPortSelectionListener, ) @@ -417,7 +430,7 @@ class VpnSettingsScreenTest { fun testShowWireguardCustomPort() = composeExtension.use { // Arrange - initScreen(state = createDefaultUiState(customWireguardPort = Port(4000))) + initScreen(state = createDefaultUiState(customWireguardPort = Port(4000)).toLc()) // Act onNodeWithTag(LAZY_LIST_VPN_SETTINGS_TEST_TAG) @@ -435,9 +448,10 @@ class VpnSettingsScreenTest { initScreen( state = createDefaultUiState( - selectedWireguardPort = Constraint.Only(Port(4000)), - customWireguardPort = Port(4000), - ), + selectedWireguardPort = Constraint.Only(Port(4000)), + customWireguardPort = Port(4000), + ) + .toLc(), onWireguardPortSelected = onWireguardPortSelected, ) @@ -457,7 +471,10 @@ class VpnSettingsScreenTest { composeExtension.use { // Arrange val mockedClickHandler: (Mtu?) -> Unit = mockk(relaxed = true) - initScreen(state = createDefaultUiState(), navigateToMtuDialog = mockedClickHandler) + initScreen( + state = createDefaultUiState().toLc(), + navigateToMtuDialog = mockedClickHandler, + ) onNodeWithTag(LAZY_LIST_VPN_SETTINGS_TEST_TAG) .performScrollToNode(hasTestTag(LAZY_LIST_LAST_ITEM_TEST_TAG)) @@ -477,9 +494,10 @@ class VpnSettingsScreenTest { initScreen( state = createDefaultUiState( - isCustomDnsEnabled = true, - customDnsItems = listOf(CustomDnsItem("1.1.1.1", false, false)), - ), + isCustomDnsEnabled = true, + customDnsItems = listOf(CustomDnsItem("1.1.1.1", false, false)), + ) + .toLc(), navigateToDns = mockedClickHandler, ) @@ -497,7 +515,7 @@ class VpnSettingsScreenTest { // Arrange initScreen( - state = createDefaultUiState(), + state = createDefaultUiState().toLc(), navigateToObfuscationInfo = mockedNavigateToObfuscationInfo, ) @@ -517,7 +535,7 @@ class VpnSettingsScreenTest { // Arrange initScreen( - state = createDefaultUiState(), + state = createDefaultUiState().toLc(), navigateToQuantumResistanceInfo = mockedShowTunnelQuantumInfoClick, ) @@ -537,7 +555,7 @@ class VpnSettingsScreenTest { // Arrange initScreen( - state = createDefaultUiState(), + state = createDefaultUiState().toLc(), navigateToWireguardPortInfo = mockedClickHandler, ) @@ -555,7 +573,7 @@ class VpnSettingsScreenTest { // Arrange initScreen( - state = createDefaultUiState(availablePortRanges = availablePortRanges), + state = createDefaultUiState(availablePortRanges = availablePortRanges).toLc(), navigateToWireguardPortDialog = mockedClickHandler, ) @@ -574,7 +592,7 @@ class VpnSettingsScreenTest { val mockOnShowCustomPortDialog: (Port?, List<PortRange>) -> Unit = mockk(relaxed = true) val availablePortRanges = listOf(Port(4000)..Port(5000)) initScreen( - state = createDefaultUiState(availablePortRanges = availablePortRanges), + state = createDefaultUiState(availablePortRanges = availablePortRanges).toLc(), navigateToWireguardPortDialog = mockOnShowCustomPortDialog, ) @@ -598,10 +616,11 @@ class VpnSettingsScreenTest { initScreen( state = createDefaultUiState( - selectedWireguardPort = Constraint.Only(customPort), - customWireguardPort = customPort, - availablePortRanges = availablePortRanges, - ), + selectedWireguardPort = Constraint.Only(customPort), + customWireguardPort = customPort, + availablePortRanges = availablePortRanges, + ) + .toLc(), navigateToWireguardPortDialog = mockOnShowCustomPortDialog, ) @@ -618,7 +637,7 @@ class VpnSettingsScreenTest { fun ensureConnectOnStartIsShownWhenSystemVpnSettingsAvailableIsFalse() = composeExtension.use { // Arrange - initScreen(state = createDefaultUiState(systemVpnSettingsAvailable = false)) + initScreen(state = createDefaultUiState(systemVpnSettingsAvailable = false).toLc()) // Assert onNodeWithText("Connect on device start-up").assertExists() @@ -632,9 +651,10 @@ class VpnSettingsScreenTest { initScreen( state = createDefaultUiState( - systemVpnSettingsAvailable = false, - autoStartAndConnectOnBoot = false, - ), + systemVpnSettingsAvailable = false, + autoStartAndConnectOnBoot = false, + ) + .toLc(), onToggleAutoStartAndConnectOnBoot = mockOnToggleAutoStartAndConnectOnBoot, ) diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/AppInfoUiStatePreviewParameterProvider.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/AppInfoUiStatePreviewParameterProvider.kt index bd11a5f654..8fa9dce994 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/AppInfoUiStatePreviewParameterProvider.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/AppInfoUiStatePreviewParameterProvider.kt @@ -2,18 +2,23 @@ package net.mullvad.mullvadvpn.compose.preview import androidx.compose.ui.tooling.preview.PreviewParameterProvider import net.mullvad.mullvadvpn.lib.model.VersionInfo +import net.mullvad.mullvadvpn.util.Lc +import net.mullvad.mullvadvpn.util.toLc import net.mullvad.mullvadvpn.viewmodel.AppInfoUiState -class AppInfoUiStatePreviewParameterProvider : PreviewParameterProvider<AppInfoUiState> { - override val values: Sequence<AppInfoUiState> = +class AppInfoUiStatePreviewParameterProvider : PreviewParameterProvider<Lc<Unit, AppInfoUiState>> { + override val values: Sequence<Lc<Unit, AppInfoUiState>> = sequenceOf( + Lc.Loading(Unit), AppInfoUiState( - version = VersionInfo(currentVersion = "2024.9", isSupported = true), - isPlayBuild = true, - ), + version = VersionInfo(currentVersion = "2024.9", isSupported = true), + isPlayBuild = true, + ) + .toLc(), AppInfoUiState( - version = VersionInfo(currentVersion = "2024.9", isSupported = false), - isPlayBuild = true, - ), + version = VersionInfo(currentVersion = "2024.9", isSupported = false), + isPlayBuild = true, + ) + .toLc(), ) } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/DaitaUiStatePreviewParameterProvider.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/DaitaUiStatePreviewParameterProvider.kt new file mode 100644 index 0000000000..3ab24a9308 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/DaitaUiStatePreviewParameterProvider.kt @@ -0,0 +1,15 @@ +package net.mullvad.mullvadvpn.compose.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import net.mullvad.mullvadvpn.compose.state.DaitaUiState +import net.mullvad.mullvadvpn.util.Lc +import net.mullvad.mullvadvpn.util.toLc + +class DaitaUiStatePreviewParameterProvider : PreviewParameterProvider<Lc<Boolean, DaitaUiState>> { + override val values: Sequence<Lc<Boolean, DaitaUiState>> = + sequenceOf( + Lc.Loading(true), + DaitaUiState(daitaEnabled = true, directOnly = false, isModal = false).toLc(), + DaitaUiState(daitaEnabled = true, directOnly = true, isModal = true).toLc(), + ) +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/MultihopUiStatePreviewParameterProvider.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/MultihopUiStatePreviewParameterProvider.kt new file mode 100644 index 0000000000..cf9394af31 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/MultihopUiStatePreviewParameterProvider.kt @@ -0,0 +1,15 @@ +package net.mullvad.mullvadvpn.compose.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import net.mullvad.mullvadvpn.util.Lc +import net.mullvad.mullvadvpn.viewmodel.MultihopUiState + +class MultihopUiStatePreviewParameterProvider : + PreviewParameterProvider<Lc<Boolean, MultihopUiState>> { + override val values: Sequence<Lc<Boolean, MultihopUiState>> = + sequenceOf( + Lc.Loading(false), + Lc.Content(MultihopUiState(enable = true, isModal = false)), + Lc.Content(MultihopUiState(enable = false, isModal = true)), + ) +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/ServerIpOverridesUiStatePreviewParameterProvider.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/ServerIpOverridesUiStatePreviewParameterProvider.kt index a69d3a4432..391f69b3c8 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/ServerIpOverridesUiStatePreviewParameterProvider.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/ServerIpOverridesUiStatePreviewParameterProvider.kt @@ -1,14 +1,16 @@ package net.mullvad.mullvadvpn.compose.preview import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import net.mullvad.mullvadvpn.util.Lc +import net.mullvad.mullvadvpn.util.toLc import net.mullvad.mullvadvpn.viewmodel.ServerIpOverridesUiState class ServerIpOverridesUiStatePreviewParameterProvider : - PreviewParameterProvider<ServerIpOverridesUiState> { + PreviewParameterProvider<Lc<Boolean, ServerIpOverridesUiState>> { override val values = sequenceOf( - ServerIpOverridesUiState.Loaded(overridesActive = true), - ServerIpOverridesUiState.Loaded(overridesActive = false), - ServerIpOverridesUiState.Loading(), + ServerIpOverridesUiState(overridesActive = true).toLc(), + ServerIpOverridesUiState(overridesActive = false).toLc(), + Lc.Loading(true), ) } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SettingsUiStatePreviewParameterProvider.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SettingsUiStatePreviewParameterProvider.kt index 5a7a6b276a..9259950d7b 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SettingsUiStatePreviewParameterProvider.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SettingsUiStatePreviewParameterProvider.kt @@ -2,25 +2,31 @@ package net.mullvad.mullvadvpn.compose.preview import androidx.compose.ui.tooling.preview.PreviewParameterProvider import net.mullvad.mullvadvpn.compose.state.SettingsUiState +import net.mullvad.mullvadvpn.util.Lc +import net.mullvad.mullvadvpn.util.toLc -class SettingsUiStatePreviewParameterProvider : PreviewParameterProvider<SettingsUiState> { +class SettingsUiStatePreviewParameterProvider : + PreviewParameterProvider<Lc<Unit, SettingsUiState>> { override val values = sequenceOf( + Lc.Loading(Unit), SettingsUiState( - appVersion = "2222.22", - isLoggedIn = true, - isSupportedVersion = true, - isDaitaEnabled = true, - isPlayBuild = true, - multihopEnabled = false, - ), + appVersion = "2222.22", + isLoggedIn = true, + isSupportedVersion = true, + isDaitaEnabled = true, + isPlayBuild = true, + multihopEnabled = false, + ) + .toLc(), SettingsUiState( - appVersion = "9000.1", - isLoggedIn = false, - isSupportedVersion = false, - isDaitaEnabled = false, - isPlayBuild = false, - multihopEnabled = false, - ), + appVersion = "9000.1", + isLoggedIn = false, + isSupportedVersion = false, + isDaitaEnabled = false, + isPlayBuild = false, + multihopEnabled = false, + ) + .toLc(), ) } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/ShadowsocksSettingsUiStatePreviewParameterProvider.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/ShadowsocksSettingsUiStatePreviewParameterProvider.kt new file mode 100644 index 0000000000..78b7c7f8bd --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/ShadowsocksSettingsUiStatePreviewParameterProvider.kt @@ -0,0 +1,18 @@ +package net.mullvad.mullvadvpn.compose.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import net.mullvad.mullvadvpn.compose.state.ShadowsocksSettingsUiState +import net.mullvad.mullvadvpn.lib.model.Constraint +import net.mullvad.mullvadvpn.lib.model.Port +import net.mullvad.mullvadvpn.util.Lc +import net.mullvad.mullvadvpn.util.toLc + +class ShadowsocksSettingsUiStatePreviewParameterProvider : + PreviewParameterProvider<Lc<Unit, ShadowsocksSettingsUiState>> { + override val values: Sequence<Lc<Unit, ShadowsocksSettingsUiState>> = + sequenceOf( + Lc.Loading(Unit), + ShadowsocksSettingsUiState(port = Constraint.Any).toLc(), + ShadowsocksSettingsUiState(port = Constraint.Only(Port(1)), customPort = Port(1)).toLc(), + ) +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SplitTunnelingUiStatePreviewParameterProvider.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SplitTunnelingUiStatePreviewParameterProvider.kt index dee2d25733..02e7cf8b14 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SplitTunnelingUiStatePreviewParameterProvider.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SplitTunnelingUiStatePreviewParameterProvider.kt @@ -3,37 +3,41 @@ package net.mullvad.mullvadvpn.compose.preview import androidx.compose.ui.tooling.preview.PreviewParameterProvider import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.applist.AppData -import net.mullvad.mullvadvpn.compose.state.SplitTunnelingUiState +import net.mullvad.mullvadvpn.util.Lc +import net.mullvad.mullvadvpn.util.toLc +import net.mullvad.mullvadvpn.viewmodel.Loading +import net.mullvad.mullvadvpn.viewmodel.SplitTunnelingUiState class SplitTunnelingUiStatePreviewParameterProvider : - PreviewParameterProvider<SplitTunnelingUiState> { + PreviewParameterProvider<Lc<Loading, SplitTunnelingUiState>> { override val values = sequenceOf( - SplitTunnelingUiState.ShowAppList( - enabled = true, - excludedApps = - listOf( - AppData( - packageName = "my.package.a", - name = "TitleA", - iconRes = R.drawable.ic_icons_missing, + SplitTunnelingUiState( + enabled = true, + excludedApps = + listOf( + AppData( + packageName = "my.package.a", + name = "TitleA", + iconRes = R.drawable.ic_icons_missing, + ), + AppData( + packageName = "my.package.b", + name = "TitleB", + iconRes = R.drawable.ic_icons_missing, + ), ), - AppData( - packageName = "my.package.b", - name = "TitleB", - iconRes = R.drawable.ic_icons_missing, + includedApps = + listOf( + AppData( + packageName = "my.package.c", + name = "TitleC", + iconRes = R.drawable.ic_icons_missing, + ) ), - ), - includedApps = - listOf( - AppData( - packageName = "my.package.c", - name = "TitleC", - iconRes = R.drawable.ic_icons_missing, - ) - ), - showSystemApps = true, - ), - SplitTunnelingUiState.Loading(enabled = true), + showSystemApps = true, + ) + .toLc(), + Lc.Loading(Loading(enabled = true)), ) } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/Udp2TcpSettingsUiStatePreviewParameterProvider.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/Udp2TcpSettingsUiStatePreviewParameterProvider.kt new file mode 100644 index 0000000000..a17b24dd90 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/Udp2TcpSettingsUiStatePreviewParameterProvider.kt @@ -0,0 +1,18 @@ +package net.mullvad.mullvadvpn.compose.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import net.mullvad.mullvadvpn.compose.state.Udp2TcpSettingsUiState +import net.mullvad.mullvadvpn.constant.UDP2TCP_PRESET_PORTS +import net.mullvad.mullvadvpn.lib.model.Constraint +import net.mullvad.mullvadvpn.util.Lc +import net.mullvad.mullvadvpn.util.toLc + +class Udp2TcpSettingsUiStatePreviewParameterProvider : + PreviewParameterProvider<Lc<Unit, Udp2TcpSettingsUiState>> { + override val values = + sequenceOf( + Lc.Loading(Unit), + Udp2TcpSettingsUiState(port = Constraint.Any).toLc(), + Udp2TcpSettingsUiState(port = Constraint.Only(UDP2TCP_PRESET_PORTS.first())).toLc(), + ) +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/VpnSettingsUiStatePreviewParameterProvider.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/VpnSettingsUiStatePreviewParameterProvider.kt index 060d67ace0..0953b5d0ad 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/VpnSettingsUiStatePreviewParameterProvider.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/VpnSettingsUiStatePreviewParameterProvider.kt @@ -1,50 +1,54 @@ package net.mullvad.mullvadvpn.compose.preview import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import net.mullvad.mullvadvpn.compose.state.CustomDnsItem +import net.mullvad.mullvadvpn.compose.state.VpnSettingsUiState import net.mullvad.mullvadvpn.lib.model.Constraint import net.mullvad.mullvadvpn.lib.model.DefaultDnsOptions import net.mullvad.mullvadvpn.lib.model.Mtu import net.mullvad.mullvadvpn.lib.model.ObfuscationMode import net.mullvad.mullvadvpn.lib.model.Port import net.mullvad.mullvadvpn.lib.model.QuantumResistantState -import net.mullvad.mullvadvpn.viewmodel.CustomDnsItem -import net.mullvad.mullvadvpn.viewmodel.VpnSettingsUiState +import net.mullvad.mullvadvpn.util.Lc +import net.mullvad.mullvadvpn.util.toLc private const val MTU = 1337 @Suppress("MagicNumber") private val PORT1 = Port(9001) @Suppress("MagicNumber") private val PORT2 = Port(12433) -class VpnSettingsUiStatePreviewParameterProvider : PreviewParameterProvider<VpnSettingsUiState> { +class VpnSettingsUiStatePreviewParameterProvider : + PreviewParameterProvider<Lc<Boolean, VpnSettingsUiState>> { override val values = sequenceOf( - VpnSettingsUiState.Loading(), - VpnSettingsUiState.Content.from( - mtu = Mtu(MTU), - isLocalNetworkSharingEnabled = true, - isCustomDnsEnabled = true, - customDnsItems = listOf(CustomDnsItem("0.0.0.0", false, false)), - contentBlockersOptions = - DefaultDnsOptions( - blockAds = true, - blockMalware = true, - blockGambling = true, - blockTrackers = true, - blockSocialMedia = true, - blockAdultContent = true, - ), - quantumResistant = QuantumResistantState.On, - selectedWireguardPort = Constraint.Any, - customWireguardPort = PORT1, - availablePortRanges = listOf(PORT1..PORT2), - systemVpnSettingsAvailable = true, - autoStartAndConnectOnBoot = true, - isIpv6Enabled = true, - obfuscationMode = ObfuscationMode.Udp2Tcp, - selectedUdp2TcpObfuscationPort = Constraint.Any, - selectedShadowsocksObfuscationPort = Constraint.Any, - isContentBlockersExpanded = true, - deviceIpVersion = Constraint.Any, - isModal = false, - ), + Lc.Loading(true), + VpnSettingsUiState.from( + mtu = Mtu(MTU), + isLocalNetworkSharingEnabled = true, + isCustomDnsEnabled = true, + customDnsItems = listOf(CustomDnsItem("0.0.0.0", false, false)), + contentBlockersOptions = + DefaultDnsOptions( + blockAds = true, + blockMalware = true, + blockGambling = true, + blockTrackers = true, + blockSocialMedia = true, + blockAdultContent = true, + ), + quantumResistant = QuantumResistantState.On, + selectedWireguardPort = Constraint.Any, + customWireguardPort = PORT1, + availablePortRanges = listOf(PORT1..PORT2), + systemVpnSettingsAvailable = true, + autoStartAndConnectOnBoot = true, + isIpv6Enabled = true, + obfuscationMode = ObfuscationMode.Udp2Tcp, + selectedUdp2TcpObfuscationPort = Constraint.Any, + selectedShadowsocksObfuscationPort = Constraint.Any, + isContentBlockersExpanded = true, + deviceIpVersion = Constraint.Any, + isModal = false, + ) + .toLc(), ) } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AppInfoScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AppInfoScreen.kt index 535e148096..295244019c 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AppInfoScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AppInfoScreen.kt @@ -31,6 +31,7 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.compose.cell.NavigationComposeCell import net.mullvad.mullvadvpn.compose.cell.TwoRowCell +import net.mullvad.mullvadvpn.compose.component.MullvadCircularProgressIndicatorLarge import net.mullvad.mullvadvpn.compose.component.NavigateBackIconButton import net.mullvad.mullvadvpn.compose.component.ScaffoldWithMediumTopBar import net.mullvad.mullvadvpn.compose.extensions.safeOpenUri @@ -40,16 +41,17 @@ import net.mullvad.mullvadvpn.compose.util.CollectSideEffectWithLifecycle import net.mullvad.mullvadvpn.compose.util.showSnackbarImmediately import net.mullvad.mullvadvpn.lib.theme.AppTheme import net.mullvad.mullvadvpn.lib.theme.Dimens +import net.mullvad.mullvadvpn.util.Lc import net.mullvad.mullvadvpn.viewmodel.AppInfoSideEffect import net.mullvad.mullvadvpn.viewmodel.AppInfoUiState import net.mullvad.mullvadvpn.viewmodel.AppInfoViewModel import org.koin.androidx.compose.koinViewModel @OptIn(ExperimentalMaterial3Api::class) -@Preview("Initial|Unsupported") +@Preview("Loading|Supported|Unsupported") @Composable private fun PreviewAppInfoScreen( - @PreviewParameter(AppInfoUiStatePreviewParameterProvider::class) state: AppInfoUiState + @PreviewParameter(AppInfoUiStatePreviewParameterProvider::class) state: Lc<Unit, AppInfoUiState> ) { AppTheme { AppInfo( @@ -95,7 +97,7 @@ fun AppInfo(navigator: DestinationsNavigator) { @ExperimentalMaterial3Api @Composable fun AppInfo( - state: AppInfoUiState, + state: Lc<Unit, AppInfoUiState>, snackbarHostState: SnackbarHostState, onBackClick: () -> Unit, navigateToChangelog: () -> Unit, @@ -106,14 +108,25 @@ fun AppInfo( navigationIcon = { NavigateBackIconButton(onNavigateBack = onBackClick) }, snackbarHostState = snackbarHostState, ) { modifier -> - Column(horizontalAlignment = Alignment.Start, modifier = modifier.animateContentSize()) { - AppInfoContent(state, navigateToChangelog, openAppListing) + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier.animateContentSize(), + ) { + when (state) { + is Lc.Loading -> Loading() + is Lc.Content -> + AppInfoContent( + state = state.value, + navigateToChangelog = navigateToChangelog, + openAppListing = openAppListing, + ) + } } } } @Composable -fun AppInfoContent( +private fun AppInfoContent( state: AppInfoUiState, navigateToChangelog: () -> Unit, openAppListing: () -> Unit, @@ -176,3 +189,8 @@ private fun ChangelogRow(navigateToChangelog: () -> Unit) { onClick = navigateToChangelog, ) } + +@Composable +private fun Loading() { + MullvadCircularProgressIndicatorLarge() +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/DaitaScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/DaitaScreen.kt index 7d451b9253..c93d9efab1 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/DaitaScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/DaitaScreen.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.dropUnlessResumed import com.ramcosta.composedestinations.annotation.Destination @@ -43,10 +44,12 @@ import kotlinx.parcelize.Parcelize import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.compose.cell.HeaderSwitchComposeCell import net.mullvad.mullvadvpn.compose.cell.SwitchComposeSubtitleCell +import net.mullvad.mullvadvpn.compose.component.MullvadCircularProgressIndicatorLarge import net.mullvad.mullvadvpn.compose.component.NavigateBackIconButton import net.mullvad.mullvadvpn.compose.component.NavigateCloseIconButton import net.mullvad.mullvadvpn.compose.component.ScaffoldWithMediumTopBar import net.mullvad.mullvadvpn.compose.dialog.info.Confirmed +import net.mullvad.mullvadvpn.compose.preview.DaitaUiStatePreviewParameterProvider import net.mullvad.mullvadvpn.compose.state.DaitaUiState import net.mullvad.mullvadvpn.compose.transitions.SlideInFromRightTransition import net.mullvad.mullvadvpn.compose.util.OnNavResultValue @@ -54,15 +57,18 @@ import net.mullvad.mullvadvpn.lib.model.FeatureIndicator import net.mullvad.mullvadvpn.lib.theme.AppTheme import net.mullvad.mullvadvpn.lib.theme.Dimens import net.mullvad.mullvadvpn.lib.ui.tag.DAITA_SCREEN_TEST_TAG +import net.mullvad.mullvadvpn.util.Lc import net.mullvad.mullvadvpn.viewmodel.DaitaViewModel import org.koin.androidx.compose.koinViewModel -@Preview +@Preview("Loading|Disabled|Enabled") @Composable -private fun PreviewDaitaScreen() { +private fun PreviewDaitaScreen( + @PreviewParameter(DaitaUiStatePreviewParameterProvider::class) state: Lc<Boolean, DaitaUiState> +) { AppTheme { DaitaScreen( - state = DaitaUiState(daitaEnabled = false, directOnly = false), + state = state, onDaitaEnabled = { _ -> }, onDirectOnlyClick = { _ -> }, onDirectOnlyInfoClick = {}, @@ -111,7 +117,7 @@ fun SharedTransitionScope.Daita( @Composable fun DaitaScreen( - state: DaitaUiState, + state: Lc<Boolean, DaitaUiState>, onDaitaEnabled: (enable: Boolean) -> Unit, onDirectOnlyClick: (enable: Boolean) -> Unit, onDirectOnlyInfoClick: () -> Unit, @@ -122,35 +128,57 @@ fun DaitaScreen( appBarTitle = stringResource(id = R.string.daita), modifier = modifier, navigationIcon = { - if (state.isModal) { + if (state.isModal()) { NavigateCloseIconButton { onBackClick() } } else { NavigateBackIconButton { onBackClick() } } }, ) { modifier -> - Column(modifier = modifier) { - val pagerState = rememberPagerState(pageCount = { DaitaPages.entries.size }) - DescriptionPager(pagerState = pagerState) - PageIndicator(pagerState = pagerState) - HeaderSwitchComposeCell( - title = stringResource(R.string.enable), - isToggled = state.daitaEnabled, - onCellClicked = onDaitaEnabled, - ) - HorizontalDivider() - HeaderSwitchComposeCell( - title = stringResource(R.string.direct_only), - isToggled = state.directOnly, - isEnabled = state.daitaEnabled, - onCellClicked = onDirectOnlyClick, - onInfoClicked = onDirectOnlyInfoClick, - ) + Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier) { + when (state) { + is Lc.Loading -> { + Loading() + } + is Lc.Content -> { + DaitaContent( + state = state.value, + onDaitaEnabled = onDaitaEnabled, + onDirectOnlyClick = onDirectOnlyClick, + onDirectOnlyInfoClick = onDirectOnlyInfoClick, + ) + } + } } } } @Composable +private fun DaitaContent( + state: DaitaUiState, + onDaitaEnabled: (enable: Boolean) -> Unit, + onDirectOnlyClick: (enable: Boolean) -> Unit, + onDirectOnlyInfoClick: () -> Unit, +) { + val pagerState = rememberPagerState(pageCount = { DaitaPages.entries.size }) + DescriptionPager(pagerState = pagerState) + PageIndicator(pagerState = pagerState) + HeaderSwitchComposeCell( + title = stringResource(R.string.enable), + isToggled = state.daitaEnabled, + onCellClicked = onDaitaEnabled, + ) + HorizontalDivider() + HeaderSwitchComposeCell( + title = stringResource(R.string.direct_only), + isToggled = state.directOnly, + isEnabled = state.daitaEnabled, + onCellClicked = onDirectOnlyClick, + onInfoClicked = onDirectOnlyInfoClick, + ) +} + +@Composable private fun DescriptionPager(pagerState: PagerState) { HorizontalPager( state = pagerState, @@ -221,6 +249,17 @@ private fun PageIndicator(pagerState: PagerState) { } } +@Composable +private fun Loading() { + MullvadCircularProgressIndicatorLarge() +} + +private fun Lc<Boolean, DaitaUiState>.isModal() = + when (this) { + is Lc.Loading -> this.value + is Lc.Content -> this.value.isModal + } + private enum class DaitaPages( val image: Int, val textFirstParagraph: @Composable () -> String, diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/MultihopScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/MultihopScreen.kt index 769b028a70..56b1c4a7b7 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/MultihopScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/MultihopScreen.kt @@ -8,6 +8,7 @@ import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.SharedTransitionScope import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.widthIn @@ -20,6 +21,7 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.dropUnlessResumed import com.ramcosta.composedestinations.annotation.Destination @@ -29,23 +31,27 @@ import kotlinx.parcelize.Parcelize import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.compose.cell.HeaderSwitchComposeCell import net.mullvad.mullvadvpn.compose.cell.SwitchComposeSubtitleCell +import net.mullvad.mullvadvpn.compose.component.MullvadCircularProgressIndicatorLarge import net.mullvad.mullvadvpn.compose.component.NavigateBackIconButton import net.mullvad.mullvadvpn.compose.component.NavigateCloseIconButton import net.mullvad.mullvadvpn.compose.component.ScaffoldWithMediumTopBar +import net.mullvad.mullvadvpn.compose.preview.MultihopUiStatePreviewParameterProvider import net.mullvad.mullvadvpn.compose.transitions.SlideInFromRightTransition import net.mullvad.mullvadvpn.lib.model.FeatureIndicator import net.mullvad.mullvadvpn.lib.theme.AppTheme import net.mullvad.mullvadvpn.lib.theme.Dimens +import net.mullvad.mullvadvpn.util.Lc import net.mullvad.mullvadvpn.viewmodel.MultihopUiState import net.mullvad.mullvadvpn.viewmodel.MultihopViewModel import org.koin.androidx.compose.koinViewModel -@Preview +@Preview("Loading|Enabled|Disabled") @Composable -private fun PreviewMultihopScreen() { - AppTheme { - MultihopScreen(state = MultihopUiState(false), onMultihopClick = {}, onBackClick = {}) - } +private fun PreviewMultihopScreen( + @PreviewParameter(MultihopUiStatePreviewParameterProvider::class) + state: Lc<Boolean, MultihopUiState> +) { + AppTheme { MultihopScreen(state = state, onMultihopClick = {}, onBackClick = {}) } } @Parcelize data class MultihopNavArgs(val isModal: Boolean = false) : Parcelable @@ -74,7 +80,7 @@ fun SharedTransitionScope.Multihop( @Composable fun MultihopScreen( - state: MultihopUiState, + state: Lc<Boolean, MultihopUiState>, onMultihopClick: (enable: Boolean) -> Unit, onBackClick: () -> Unit, modifier: Modifier = Modifier, @@ -83,36 +89,49 @@ fun MultihopScreen( modifier = modifier, appBarTitle = stringResource(id = R.string.multihop), navigationIcon = { - if (state.isModal) { + if (state.isModal()) { NavigateCloseIconButton(onBackClick) } else { NavigateBackIconButton(onNavigateBack = onBackClick) } }, ) { modifier -> - Column(modifier = modifier) { - // Scale image to fit width up to certain width - Image( - contentScale = ContentScale.FillWidth, - modifier = - Modifier.widthIn(max = Dimens.settingsDetailsImageMaxWidth) - .fillMaxWidth() - .padding(horizontal = Dimens.mediumPadding) - .align(Alignment.CenterHorizontally), - painter = painterResource(id = R.drawable.multihop_illustration), - contentDescription = stringResource(R.string.multihop), - ) - Description() - HeaderSwitchComposeCell( - title = stringResource(R.string.enable), - isToggled = state.enable, - onCellClicked = onMultihopClick, - ) + Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier) { + when (state) { + is Lc.Loading -> Loading() + is Lc.Content -> { + MultihopContent(state = state.value, onMultihopClick = onMultihopClick) + } + } } } } @Composable +private fun ColumnScope.MultihopContent( + state: MultihopUiState, + onMultihopClick: (enable: Boolean) -> Unit, +) { + // Scale image to fit width up to certain width + Image( + contentScale = ContentScale.FillWidth, + modifier = + Modifier.widthIn(max = Dimens.settingsDetailsImageMaxWidth) + .fillMaxWidth() + .padding(horizontal = Dimens.mediumPadding) + .align(Alignment.CenterHorizontally), + painter = painterResource(id = R.drawable.multihop_illustration), + contentDescription = stringResource(R.string.multihop), + ) + Description() + HeaderSwitchComposeCell( + title = stringResource(R.string.enable), + isToggled = state.enable, + onCellClicked = onMultihopClick, + ) +} + +@Composable private fun Description() { SwitchComposeSubtitleCell( modifier = Modifier.padding(vertical = Dimens.mediumPadding), @@ -120,3 +139,14 @@ private fun Description() { text = stringResource(R.string.multihop_description), ) } + +@Composable +private fun Loading() { + MullvadCircularProgressIndicatorLarge() +} + +private fun Lc<Boolean, MultihopUiState>.isModal(): Boolean = + when (this) { + is Lc.Loading -> this.value + is Lc.Content -> this.value.isModal + } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ServerIpOverridesScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ServerIpOverridesScreen.kt index 78cd0e371d..90bedbeb18 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ServerIpOverridesScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ServerIpOverridesScreen.kt @@ -85,6 +85,7 @@ import net.mullvad.mullvadvpn.lib.ui.tag.SERVER_IP_OVERRIDE_IMPORT_TEST_TAG import net.mullvad.mullvadvpn.lib.ui.tag.SERVER_IP_OVERRIDE_INFO_TEST_TAG import net.mullvad.mullvadvpn.lib.ui.tag.SERVER_IP_OVERRIDE_MORE_VERT_TEST_TAG import net.mullvad.mullvadvpn.lib.ui.tag.SERVER_IP_OVERRIDE_RESET_OVERRIDES_TEST_TAG +import net.mullvad.mullvadvpn.util.Lc import net.mullvad.mullvadvpn.viewmodel.ServerIpOverridesUiSideEffect import net.mullvad.mullvadvpn.viewmodel.ServerIpOverridesUiState import net.mullvad.mullvadvpn.viewmodel.ServerIpOverridesViewModel @@ -94,7 +95,7 @@ import org.koin.androidx.compose.koinViewModel @Composable private fun PreviewServerIpOverridesScreen( @PreviewParameter(ServerIpOverridesUiStatePreviewParameterProvider::class) - state: ServerIpOverridesUiState + state: Lc<Boolean, ServerIpOverridesUiState> ) { AppTheme { ServerIpOverridesScreen( @@ -185,7 +186,7 @@ fun SharedTransitionScope.ServerIpOverrides( @OptIn(ExperimentalMaterial3Api::class) @Composable fun ServerIpOverridesScreen( - state: ServerIpOverridesUiState, + state: Lc<Boolean, ServerIpOverridesUiState>, onBackClick: () -> Unit, onInfoClick: () -> Unit, onResetOverridesClick: () -> Unit, @@ -202,7 +203,7 @@ fun ServerIpOverridesScreen( appBarTitle = stringResource(id = R.string.server_ip_override), modifier = modifier, navigationIcon = { - if (state.isModal) { + if (state.isModal()) { NavigateCloseIconButton(onBackClick) } else { NavigateBackIconButton(onNavigateBack = onBackClick) @@ -210,24 +211,24 @@ fun ServerIpOverridesScreen( }, actions = { TopBarActions( - overridesActive = state.overridesActive, + overridesActive = state.contentOrNull()?.overridesActive, onInfoClick = onInfoClick, onResetOverridesClick = onResetOverridesClick, ) }, ) { modifier -> - if (showBottomSheet && state.overridesActive != null) { + if (showBottomSheet && state is Lc.Content) { ImportOverridesByBottomSheet( sheetState, { showBottomSheet = it }, - state.overridesActive!!, + state.value.overridesActive, onImportByFile, onImportByText, ) } Column(modifier = modifier.animateContentSize()) { - ServerIpOverridesCell(active = state.overridesActive) + ServerIpOverridesCell(active = state.contentOrNull()?.overridesActive) Spacer(modifier = Modifier.weight(1f)) SnackbarHost(hostState = snackbarHostState) { MullvadSnackbar(snackbarData = it) } @@ -348,7 +349,7 @@ private fun TopBarActions( showMenu = false onResetOverridesClick() }, - enabled = overridesActive ?: false, + enabled = overridesActive == true, colors = MenuDefaults.itemColors( leadingIconColor = MaterialTheme.colorScheme.onPrimary, @@ -361,6 +362,12 @@ private fun TopBarActions( } } +private fun Lc<Boolean, ServerIpOverridesUiState>.isModal(): Boolean = + when (this) { + is Lc.Loading -> this.value + is Lc.Content -> this.value.isModal + } + private fun SettingsPatchError?.toString(context: Context) = when (this) { SettingsPatchError.DeserializePatched -> diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SettingsScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SettingsScreen.kt index 2b129f8b4c..c6cd4205cf 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SettingsScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SettingsScreen.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ChevronRight import androidx.compose.material.icons.filled.Error @@ -14,6 +15,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.testTag @@ -36,6 +38,7 @@ import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.compose.cell.DefaultExternalLinkView import net.mullvad.mullvadvpn.compose.cell.NavigationComposeCell import net.mullvad.mullvadvpn.compose.cell.TwoRowCell +import net.mullvad.mullvadvpn.compose.component.MullvadCircularProgressIndicatorLarge import net.mullvad.mullvadvpn.compose.component.NavigateCloseIconButton import net.mullvad.mullvadvpn.compose.component.ScaffoldWithMediumTopBar import net.mullvad.mullvadvpn.compose.extensions.createUriHook @@ -48,15 +51,17 @@ import net.mullvad.mullvadvpn.lib.theme.Dimens import net.mullvad.mullvadvpn.lib.ui.tag.DAITA_CELL_TEST_TAG import net.mullvad.mullvadvpn.lib.ui.tag.LAZY_LIST_TEST_TAG import net.mullvad.mullvadvpn.lib.ui.tag.VPN_SETTINGS_CELL_TEST_TAG +import net.mullvad.mullvadvpn.util.Lc import net.mullvad.mullvadvpn.util.appendHideNavOnPlayBuild import net.mullvad.mullvadvpn.viewmodel.SettingsViewModel import org.koin.androidx.compose.koinViewModel @OptIn(ExperimentalMaterial3Api::class) -@Preview("Supported|+") +@Preview("Loading|Supported|+") @Composable private fun PreviewSettingsScreen( - @PreviewParameter(SettingsUiStatePreviewParameterProvider::class) state: SettingsUiState + @PreviewParameter(SettingsUiStatePreviewParameterProvider::class) + state: Lc<Unit, SettingsUiState> ) { AppTheme { SettingsScreen( @@ -96,7 +101,7 @@ fun Settings(navigator: DestinationsNavigator) { @Composable fun SettingsScreen( - state: SettingsUiState, + state: Lc<Unit, SettingsUiState>, onVpnSettingCellClick: () -> Unit, onSplitTunnelingCellClick: () -> Unit, onAppInfoClick: () -> Unit, @@ -111,52 +116,80 @@ fun SettingsScreen( navigationIcon = { NavigateCloseIconButton(onBackClick) }, ) { modifier, lazyListState -> LazyColumn( + horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier.testTag(LAZY_LIST_TEST_TAG).animateContentSize(), state = lazyListState, ) { - if (state.isLoggedIn) { - itemWithDivider { - DaitaCell(isDaitaEnabled = state.isDaitaEnabled, onDaitaClick = onDaitaClick) - } - itemWithDivider { - MultihopCell( - isMultihopEnabled = state.multihopEnabled, + when (state) { + is Lc.Loading -> loading() + is Lc.Content -> { + content( + state = state.value, + onVpnSettingCellClick = onVpnSettingCellClick, + onSplitTunnelingCellClick = onSplitTunnelingCellClick, + onAppInfoClick = onAppInfoClick, + onReportProblemCellClick = onReportProblemCellClick, + onApiAccessClick = onApiAccessClick, onMultihopClick = onMultihopClick, + onDaitaClick = onDaitaClick, ) } - itemWithDivider { - NavigationComposeCell( - title = stringResource(id = R.string.settings_vpn), - onClick = onVpnSettingCellClick, - testTag = VPN_SETTINGS_CELL_TEST_TAG, - ) - } - item { Spacer(modifier = Modifier.height(Dimens.cellVerticalSpacing)) } - item { SplitTunneling(onSplitTunnelingCellClick) } - item { Spacer(modifier = Modifier.height(Dimens.cellVerticalSpacing)) } } + } + } +} - item { - NavigationComposeCell( - title = stringResource(id = R.string.settings_api_access), - onClick = onApiAccessClick, - ) - } - item { Spacer(modifier = Modifier.height(Dimens.cellVerticalSpacing)) } +private fun LazyListScope.content( + state: SettingsUiState, + onVpnSettingCellClick: () -> Unit, + onSplitTunnelingCellClick: () -> Unit, + onAppInfoClick: () -> Unit, + onReportProblemCellClick: () -> Unit, + onApiAccessClick: () -> Unit, + onMultihopClick: () -> Unit, + onDaitaClick: () -> Unit, +) { + if (state.isLoggedIn) { + itemWithDivider { + DaitaCell(isDaitaEnabled = state.isDaitaEnabled, onDaitaClick = onDaitaClick) + } + itemWithDivider { + MultihopCell( + isMultihopEnabled = state.multihopEnabled, + onMultihopClick = onMultihopClick, + ) + } + itemWithDivider { + NavigationComposeCell( + title = stringResource(id = R.string.settings_vpn), + onClick = onVpnSettingCellClick, + testTag = VPN_SETTINGS_CELL_TEST_TAG, + ) + } + item { Spacer(modifier = Modifier.height(Dimens.cellVerticalSpacing)) } + item { SplitTunneling(onSplitTunnelingCellClick) } + item { Spacer(modifier = Modifier.height(Dimens.cellVerticalSpacing)) } + } - item { AppInfo(onAppInfoClick, state) } + item { + NavigationComposeCell( + title = stringResource(id = R.string.settings_api_access), + onClick = onApiAccessClick, + ) + } + item { Spacer(modifier = Modifier.height(Dimens.cellVerticalSpacing)) } - item { Spacer(modifier = Modifier.height(Dimens.cellVerticalSpacing)) } + item { AppInfo(onAppInfoClick, state) } - itemWithDivider { ReportProblem(onReportProblemCellClick) } + item { Spacer(modifier = Modifier.height(Dimens.cellVerticalSpacing)) } - if (!state.isPlayBuild) { - itemWithDivider { FaqAndGuides() } - } + itemWithDivider { ReportProblem(onReportProblemCellClick) } - itemWithDivider { PrivacyPolicy(state) } - } + if (!state.isPlayBuild) { + itemWithDivider { FaqAndGuides() } } + + itemWithDivider { PrivacyPolicy(state) } } @Composable @@ -288,3 +321,7 @@ private fun MultihopCell(isMultihopEnabled: Boolean, onMultihopClick: () -> Unit }, ) } + +private fun LazyListScope.loading() { + item { MullvadCircularProgressIndicatorLarge() } +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ShadowsocksSettingsScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ShadowsocksSettingsScreen.kt index 4dbdc50f7a..94e37f6945 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ShadowsocksSettingsScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ShadowsocksSettingsScreen.kt @@ -1,10 +1,13 @@ package net.mullvad.mullvadvpn.compose.screen import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.dropUnlessResumed import com.ramcosta.composedestinations.annotation.Destination @@ -16,11 +19,14 @@ import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.compose.cell.CustomPortCell import net.mullvad.mullvadvpn.compose.cell.InformationComposeCell import net.mullvad.mullvadvpn.compose.cell.SelectableCell +import net.mullvad.mullvadvpn.compose.component.MullvadCircularProgressIndicatorLarge import net.mullvad.mullvadvpn.compose.component.NavigateBackIconButton import net.mullvad.mullvadvpn.compose.component.ScaffoldWithMediumTopBar import net.mullvad.mullvadvpn.compose.dialog.CustomPortNavArgs +import net.mullvad.mullvadvpn.compose.extensions.dropUnlessResumed import net.mullvad.mullvadvpn.compose.extensions.itemWithDivider -import net.mullvad.mullvadvpn.compose.state.ShadowsocksSettingsState +import net.mullvad.mullvadvpn.compose.preview.ShadowsocksSettingsUiStatePreviewParameterProvider +import net.mullvad.mullvadvpn.compose.state.ShadowsocksSettingsUiState import net.mullvad.mullvadvpn.compose.transitions.SlideInFromRightTransition import net.mullvad.mullvadvpn.compose.util.OnNavResultValue import net.mullvad.mullvadvpn.constant.SHADOWSOCKS_AVAILABLE_PORTS @@ -31,15 +37,19 @@ import net.mullvad.mullvadvpn.lib.theme.AppTheme import net.mullvad.mullvadvpn.lib.ui.tag.SHADOWSOCKS_CUSTOM_PORT_TEXT_TEST_TAG import net.mullvad.mullvadvpn.lib.ui.tag.SHADOWSOCKS_PORT_ITEM_AUTOMATIC_TEST_TAG import net.mullvad.mullvadvpn.lib.ui.tag.SHADOWSOCKS_PORT_ITEM_X_TEST_TAG +import net.mullvad.mullvadvpn.util.Lc import net.mullvad.mullvadvpn.viewmodel.ShadowsocksSettingsViewModel import org.koin.androidx.compose.koinViewModel -@Preview +@Preview("Loading|Automatic|Custom") @Composable -private fun PreviewShadowsocksSettingsScreen() { +private fun PreviewShadowsocksSettingsScreen( + @PreviewParameter(ShadowsocksSettingsUiStatePreviewParameterProvider::class) + state: Lc<Unit, ShadowsocksSettingsUiState> +) { AppTheme { ShadowsocksSettingsScreen( - state = ShadowsocksSettingsState(port = Constraint.Any, validPortRanges = emptyList()), + state = state, navigateToCustomPortDialog = {}, onObfuscationPortSelected = {}, onBackClick = {}, @@ -67,11 +77,11 @@ fun ShadowsocksSettings( ShadowsocksSettingsScreen( state = state, navigateToCustomPortDialog = - dropUnlessResumed { + dropUnlessResumed { customPort -> navigator.navigate( ShadowsocksCustomPortDestination( CustomPortNavArgs( - customPort = state.customPort, + customPort = customPort, allowedPortRanges = SHADOWSOCKS_AVAILABLE_PORTS, ) ) @@ -84,8 +94,8 @@ fun ShadowsocksSettings( @Composable fun ShadowsocksSettingsScreen( - state: ShadowsocksSettingsState, - navigateToCustomPortDialog: () -> Unit, + state: Lc<Unit, ShadowsocksSettingsUiState>, + navigateToCustomPortDialog: (customPort: Port?) -> Unit, onObfuscationPortSelected: (Constraint<Port>) -> Unit, onBackClick: () -> Unit, ) { @@ -93,42 +103,69 @@ fun ShadowsocksSettingsScreen( appBarTitle = stringResource(id = R.string.shadowsocks), navigationIcon = { NavigateBackIconButton(onNavigateBack = onBackClick) }, ) { modifier, lazyListState -> - LazyColumn(modifier = modifier, state = lazyListState) { - itemWithDivider { InformationComposeCell(title = stringResource(R.string.port)) } - itemWithDivider { - SelectableCell( - title = stringResource(id = R.string.automatic), - isSelected = state.port is Constraint.Any, - onCellClicked = { onObfuscationPortSelected(Constraint.Any) }, - testTag = SHADOWSOCKS_PORT_ITEM_AUTOMATIC_TEST_TAG, - ) - } - SHADOWSOCKS_PRESET_PORTS.forEach { port -> - itemWithDivider { - SelectableCell( - title = port.toString(), - isSelected = state.port.getOrNull() == port, - onCellClicked = { onObfuscationPortSelected(Constraint.Only(port)) }, - testTag = String.format(null, SHADOWSOCKS_PORT_ITEM_X_TEST_TAG, port.value), + LazyColumn( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier, + state = lazyListState, + ) { + when (state) { + is Lc.Loading -> { + loading() + } + is Lc.Content -> { + content( + state = state.value, + navigateToCustomPortDialog = navigateToCustomPortDialog, + onObfuscationPortSelected = onObfuscationPortSelected, ) } } - itemWithDivider { - CustomPortCell( - title = stringResource(id = R.string.wireguard_custon_port_title), - isSelected = state.isCustom, - port = state.customPort, - onMainCellClicked = { - if (state.customPort != null) { - onObfuscationPortSelected(Constraint.Only(state.customPort)) - } else { - navigateToCustomPortDialog() - } - }, - onPortCellClicked = navigateToCustomPortDialog, - mainTestTag = SHADOWSOCKS_CUSTOM_PORT_TEXT_TEST_TAG, - ) - } } } } + +private fun LazyListScope.content( + state: ShadowsocksSettingsUiState, + navigateToCustomPortDialog: (Port?) -> Unit, + onObfuscationPortSelected: (Constraint<Port>) -> Unit, +) { + itemWithDivider { InformationComposeCell(title = stringResource(R.string.port)) } + itemWithDivider { + SelectableCell( + title = stringResource(id = R.string.automatic), + isSelected = state.port is Constraint.Any, + onCellClicked = { onObfuscationPortSelected(Constraint.Any) }, + testTag = SHADOWSOCKS_PORT_ITEM_AUTOMATIC_TEST_TAG, + ) + } + SHADOWSOCKS_PRESET_PORTS.forEach { port -> + itemWithDivider { + SelectableCell( + title = port.toString(), + isSelected = state.port.getOrNull() == port, + onCellClicked = { onObfuscationPortSelected(Constraint.Only(port)) }, + testTag = String.format(null, SHADOWSOCKS_PORT_ITEM_X_TEST_TAG, port.value), + ) + } + } + itemWithDivider { + CustomPortCell( + title = stringResource(id = R.string.wireguard_custon_port_title), + isSelected = state.isCustom, + port = state.customPort, + onMainCellClicked = { + if (state.customPort != null) { + onObfuscationPortSelected(Constraint.Only(state.customPort)) + } else { + navigateToCustomPortDialog(null) + } + }, + onPortCellClicked = { navigateToCustomPortDialog(state.customPort) }, + mainTestTag = SHADOWSOCKS_CUSTOM_PORT_TEXT_TEST_TAG, + ) + } +} + +private fun LazyListScope.loading() { + item { MullvadCircularProgressIndicatorLarge() } +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SplitTunnelingScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SplitTunnelingScreen.kt index aba3996803..de794acebd 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SplitTunnelingScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SplitTunnelingScreen.kt @@ -49,14 +49,16 @@ import net.mullvad.mullvadvpn.compose.constant.SplitTunnelingContentKey import net.mullvad.mullvadvpn.compose.extensions.itemWithDivider import net.mullvad.mullvadvpn.compose.extensions.itemsIndexedWithDivider import net.mullvad.mullvadvpn.compose.preview.SplitTunnelingUiStatePreviewParameterProvider -import net.mullvad.mullvadvpn.compose.state.SplitTunnelingUiState import net.mullvad.mullvadvpn.compose.transitions.SlideInFromRightTransition import net.mullvad.mullvadvpn.lib.model.FeatureIndicator import net.mullvad.mullvadvpn.lib.theme.AppTheme import net.mullvad.mullvadvpn.lib.theme.Dimens import net.mullvad.mullvadvpn.lib.theme.color.AlphaDisabled import net.mullvad.mullvadvpn.lib.theme.color.AlphaVisible +import net.mullvad.mullvadvpn.util.Lc import net.mullvad.mullvadvpn.util.getApplicationIconOrNull +import net.mullvad.mullvadvpn.viewmodel.Loading +import net.mullvad.mullvadvpn.viewmodel.SplitTunnelingUiState import net.mullvad.mullvadvpn.viewmodel.SplitTunnelingViewModel import org.koin.androidx.compose.koinViewModel @@ -64,7 +66,7 @@ import org.koin.androidx.compose.koinViewModel @Composable private fun PreviewSplitTunnelingScreen( @PreviewParameter(SplitTunnelingUiStatePreviewParameterProvider::class) - state: SplitTunnelingUiState + state: Lc<Loading, SplitTunnelingUiState> ) { AppTheme { SplitTunnelingScreen( @@ -114,7 +116,7 @@ fun SharedTransitionScope.SplitTunneling( @Composable fun SplitTunnelingScreen( - state: SplitTunnelingUiState, + state: Lc<Loading, SplitTunnelingUiState>, onEnableSplitTunneling: (Boolean) -> Unit, onShowSystemAppsClick: (show: Boolean) -> Unit, onExcludeAppClick: (packageName: String) -> Unit, @@ -129,7 +131,7 @@ fun SplitTunnelingScreen( modifier = modifier.fillMaxSize(), appBarTitle = stringResource(id = R.string.split_tunneling), navigationIcon = { - if (state.isModal) { + if (state.isModal()) { NavigateCloseIconButton(onNavigateClose = onBackClick) } else { NavigateBackIconButton(onNavigateBack = onBackClick) @@ -142,15 +144,18 @@ fun SplitTunnelingScreen( state = lazyListState, ) { description() - enabledToggle(enabled = state.enabled, onEnableSplitTunneling = onEnableSplitTunneling) + enabledToggle( + enabled = state.enabled(), + onEnableSplitTunneling = onEnableSplitTunneling, + ) spacer() when (state) { - is SplitTunnelingUiState.Loading -> { + is Lc.Loading -> { loading() } - is SplitTunnelingUiState.ShowAppList -> { + is Lc.Content -> { appList( - state = state, + state = state.value, focusManager = focusManager, onShowSystemAppsClick = onShowSystemAppsClick, onExcludeAppClick = onExcludeAppClick, @@ -196,7 +201,7 @@ private fun LazyListScope.loading() { } private fun LazyListScope.appList( - state: SplitTunnelingUiState.ShowAppList, + state: SplitTunnelingUiState, focusManager: FocusManager, onShowSystemAppsClick: (show: Boolean) -> Unit, onExcludeAppClick: (packageName: String) -> Unit, @@ -332,3 +337,15 @@ private fun LazyListScope.spacer() { Spacer(modifier = Modifier.animateItem().height(Dimens.mediumPadding)) } } + +private fun Lc<Loading, SplitTunnelingUiState>.isModal(): Boolean = + when (this) { + is Lc.Loading -> this.value.isModal + is Lc.Content -> this.value.isModal + } + +private fun Lc<Loading, SplitTunnelingUiState>.enabled(): Boolean = + when (this) { + is Lc.Loading -> this.value.enabled + is Lc.Content -> this.value.enabled + } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/Udp2TcpSettingsScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/Udp2TcpSettingsScreen.kt index 94a473dd5c..00a8151254 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/Udp2TcpSettingsScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/Udp2TcpSettingsScreen.kt @@ -1,10 +1,13 @@ package net.mullvad.mullvadvpn.compose.screen import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.dropUnlessResumed import com.ramcosta.composedestinations.annotation.Destination @@ -14,10 +17,12 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.compose.cell.InformationComposeCell import net.mullvad.mullvadvpn.compose.cell.SelectableCell +import net.mullvad.mullvadvpn.compose.component.MullvadCircularProgressIndicatorLarge import net.mullvad.mullvadvpn.compose.component.NavigateBackIconButton import net.mullvad.mullvadvpn.compose.component.ScaffoldWithMediumTopBar import net.mullvad.mullvadvpn.compose.extensions.itemWithDivider -import net.mullvad.mullvadvpn.compose.state.Udp2TcpSettingsState +import net.mullvad.mullvadvpn.compose.preview.Udp2TcpSettingsUiStatePreviewParameterProvider +import net.mullvad.mullvadvpn.compose.state.Udp2TcpSettingsUiState import net.mullvad.mullvadvpn.compose.transitions.SlideInFromRightTransition import net.mullvad.mullvadvpn.constant.UDP2TCP_PRESET_PORTS import net.mullvad.mullvadvpn.lib.model.Constraint @@ -25,15 +30,19 @@ import net.mullvad.mullvadvpn.lib.model.Port import net.mullvad.mullvadvpn.lib.theme.AppTheme import net.mullvad.mullvadvpn.lib.ui.tag.UDP_OVER_TCP_PORT_ITEM_AUTOMATIC_TEST_TAG import net.mullvad.mullvadvpn.lib.ui.tag.UDP_OVER_TCP_PORT_ITEM_X_TEST_TAG +import net.mullvad.mullvadvpn.util.Lc import net.mullvad.mullvadvpn.viewmodel.Udp2TcpSettingsViewModel import org.koin.androidx.compose.koinViewModel -@Preview +@Preview("Loading|Automatic|80") @Composable -private fun PreviewUdp2TcpSettingsScreen() { +private fun PreviewUdp2TcpSettingsScreen( + @PreviewParameter(Udp2TcpSettingsUiStatePreviewParameterProvider::class) + state: Lc<Unit, Udp2TcpSettingsUiState> +) { AppTheme { Udp2TcpSettingsScreen( - state = Udp2TcpSettingsState(port = Constraint.Any), + state = state, onObfuscationPortSelected = {}, navigateUdp2TcpInfo = {}, onBackClick = {}, @@ -57,7 +66,7 @@ fun Udp2TcpSettings(navigator: DestinationsNavigator) { @Composable fun Udp2TcpSettingsScreen( - state: Udp2TcpSettingsState, + state: Lc<Unit, Udp2TcpSettingsUiState>, onObfuscationPortSelected: (Constraint<Port>) -> Unit, navigateUdp2TcpInfo: () -> Unit, onBackClick: () -> Unit, @@ -66,32 +75,56 @@ fun Udp2TcpSettingsScreen( appBarTitle = stringResource(id = R.string.upd_over_tcp), navigationIcon = { NavigateBackIconButton(onNavigateBack = onBackClick) }, ) { modifier, lazyListState -> - LazyColumn(modifier = modifier, state = lazyListState) { - itemWithDivider { - InformationComposeCell( - title = stringResource(R.string.port), - onInfoClicked = navigateUdp2TcpInfo, - onCellClicked = navigateUdp2TcpInfo, - ) - } - itemWithDivider { - SelectableCell( - title = stringResource(id = R.string.automatic), - isSelected = state.port is Constraint.Any, - onCellClicked = { onObfuscationPortSelected(Constraint.Any) }, - testTag = UDP_OVER_TCP_PORT_ITEM_AUTOMATIC_TEST_TAG, - ) - } - UDP2TCP_PRESET_PORTS.forEach { port -> - itemWithDivider { - SelectableCell( - title = port.toString(), - isSelected = state.port.getOrNull() == port, - onCellClicked = { onObfuscationPortSelected(Constraint.Only(port)) }, - testTag = String.format(null, UDP_OVER_TCP_PORT_ITEM_X_TEST_TAG, port.value), + LazyColumn( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier, + state = lazyListState, + ) { + when (state) { + is Lc.Loading -> loading() + is Lc.Content -> + content( + state = state.value, + onObfuscationPortSelected = onObfuscationPortSelected, + navigateUdp2TcpInfo = navigateUdp2TcpInfo, ) - } } } } } + +private fun LazyListScope.content( + state: Udp2TcpSettingsUiState, + onObfuscationPortSelected: (Constraint<Port>) -> Unit, + navigateUdp2TcpInfo: () -> Unit, +) { + itemWithDivider { + InformationComposeCell( + title = stringResource(R.string.port), + onInfoClicked = navigateUdp2TcpInfo, + onCellClicked = navigateUdp2TcpInfo, + ) + } + itemWithDivider { + SelectableCell( + title = stringResource(id = R.string.automatic), + isSelected = state.port is Constraint.Any, + onCellClicked = { onObfuscationPortSelected(Constraint.Any) }, + testTag = UDP_OVER_TCP_PORT_ITEM_AUTOMATIC_TEST_TAG, + ) + } + UDP2TCP_PRESET_PORTS.forEach { port -> + itemWithDivider { + SelectableCell( + title = port.toString(), + isSelected = state.port.getOrNull() == port, + onCellClicked = { onObfuscationPortSelected(Constraint.Only(port)) }, + testTag = String.format(null, UDP_OVER_TCP_PORT_ITEM_X_TEST_TAG, port.value), + ) + } + } +} + +private fun LazyListScope.loading() { + item { MullvadCircularProgressIndicatorLarge() } +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreen.kt index 22ab30ff03..427d035c87 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreen.kt @@ -93,6 +93,7 @@ import net.mullvad.mullvadvpn.compose.dialog.info.WireguardPortInfoDialogArgumen import net.mullvad.mullvadvpn.compose.extensions.dropUnlessResumed import net.mullvad.mullvadvpn.compose.preview.VpnSettingsUiStatePreviewParameterProvider import net.mullvad.mullvadvpn.compose.state.VpnSettingItem +import net.mullvad.mullvadvpn.compose.state.VpnSettingsUiState import net.mullvad.mullvadvpn.compose.transitions.SlideInFromRightTransition import net.mullvad.mullvadvpn.compose.util.CollectSideEffectWithLifecycle import net.mullvad.mullvadvpn.compose.util.OnNavResultValue @@ -122,16 +123,17 @@ import net.mullvad.mullvadvpn.lib.ui.tag.LAZY_LIST_WIREGUARD_PORT_ITEM_X_TEST_TA import net.mullvad.mullvadvpn.lib.ui.tag.WIREGUARD_OBFUSCATION_OFF_CELL_TEST_TAG import net.mullvad.mullvadvpn.lib.ui.tag.WIREGUARD_OBFUSCATION_SHADOWSOCKS_CELL_TEST_TAG import net.mullvad.mullvadvpn.lib.ui.tag.WIREGUARD_OBFUSCATION_UDP_OVER_TCP_CELL_TEST_TAG +import net.mullvad.mullvadvpn.util.Lc import net.mullvad.mullvadvpn.util.indexOfFirstOrNull import net.mullvad.mullvadvpn.viewmodel.VpnSettingsSideEffect -import net.mullvad.mullvadvpn.viewmodel.VpnSettingsUiState import net.mullvad.mullvadvpn.viewmodel.VpnSettingsViewModel import org.koin.androidx.compose.koinViewModel @Preview("Default|NonDefault") @Composable private fun PreviewVpnSettings( - @PreviewParameter(VpnSettingsUiStatePreviewParameterProvider::class) state: VpnSettingsUiState + @PreviewParameter(VpnSettingsUiStatePreviewParameterProvider::class) + state: Lc<Boolean, VpnSettingsUiState> ) { AppTheme { VpnSettingsScreen( @@ -310,7 +312,7 @@ fun SharedTransitionScope.VpnSettings( @Suppress("LongParameterList") @Composable fun VpnSettingsScreen( - state: VpnSettingsUiState, + state: Lc<Boolean, VpnSettingsUiState>, initialScrollToFeature: FeatureIndicator?, modifier: Modifier = Modifier, snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }, @@ -361,7 +363,7 @@ fun VpnSettingsScreen( MullvadMediumTopBar( title = stringResource(id = R.string.settings_vpn), navigationIcon = { - if (state.isModal) { + if (state.isModal()) { NavigateCloseIconButton(onNavigateClose = onBackClick) } else { NavigateBackIconButton(onNavigateBack = onBackClick) @@ -379,12 +381,11 @@ fun VpnSettingsScreen( content = { Box(modifier = Modifier.fillMaxSize().padding(it)) { when (state) { - is VpnSettingsUiState.Loading -> - CircularProgressIndicator(modifier.align(Alignment.Center)) + is Lc.Loading -> CircularProgressIndicator(modifier.align(Alignment.Center)) - is VpnSettingsUiState.Content -> + is Lc.Content -> VpnSettingsContent( - state, + state.value, initialScrollToFeature, canScroll, navigateToContentBlockersInfo, @@ -428,7 +429,7 @@ fun VpnSettingsScreen( @Suppress("LongMethod", "LongParameterList", "CyclomaticComplexMethod") @Composable fun VpnSettingsContent( - state: VpnSettingsUiState.Content, + state: VpnSettingsUiState, initialScrollToFeature: FeatureIndicator?, canScroll: MutableState<Boolean>, navigateToContentBlockersInfo: () -> Unit, @@ -997,3 +998,9 @@ private fun VpnSettingsSideEffect.ShowToast.message(context: Context) = context.getString(R.string.settings_changes_effect_warning_short) VpnSettingsSideEffect.ShowToast.GenericError -> context.getString(R.string.error_occurred) } + +private fun Lc<Boolean, VpnSettingsUiState>.isModal() = + when (this) { + is Lc.Loading -> value + is Lc.Content -> value.isModal + } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/ShadowsocksSettingsState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/ShadowsocksSettingsUiState.kt index 7a5a0f86d5..351f7e1e02 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/ShadowsocksSettingsState.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/ShadowsocksSettingsUiState.kt @@ -2,12 +2,10 @@ package net.mullvad.mullvadvpn.compose.state import net.mullvad.mullvadvpn.lib.model.Constraint import net.mullvad.mullvadvpn.lib.model.Port -import net.mullvad.mullvadvpn.lib.model.PortRange -data class ShadowsocksSettingsState( +data class ShadowsocksSettingsUiState( val port: Constraint<Port> = Constraint.Any, val customPort: Port? = null, - val validPortRanges: List<PortRange> = emptyList(), ) { val isCustom = port is Constraint.Only && port.value == customPort } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SplitTunnelingUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SplitTunnelingUiState.kt deleted file mode 100644 index 795e69a62c..0000000000 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SplitTunnelingUiState.kt +++ /dev/null @@ -1,21 +0,0 @@ -package net.mullvad.mullvadvpn.compose.state - -import net.mullvad.mullvadvpn.applist.AppData - -sealed interface SplitTunnelingUiState { - val enabled: Boolean - val isModal: Boolean - - data class Loading( - override val enabled: Boolean = false, - override val isModal: Boolean = false, - ) : SplitTunnelingUiState - - data class ShowAppList( - override val enabled: Boolean = false, - val excludedApps: List<AppData> = emptyList(), - val includedApps: List<AppData> = emptyList(), - val showSystemApps: Boolean = false, - override val isModal: Boolean = false, - ) : SplitTunnelingUiState -} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/Udp2TcpSettingsState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/Udp2TcpSettingsUiState.kt index 1eb9c3ebd6..58d3d52396 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/Udp2TcpSettingsState.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/Udp2TcpSettingsUiState.kt @@ -3,4 +3,4 @@ package net.mullvad.mullvadvpn.compose.state import net.mullvad.mullvadvpn.lib.model.Constraint import net.mullvad.mullvadvpn.lib.model.Port -data class Udp2TcpSettingsState(val port: Constraint<Port> = Constraint.Any) +data class Udp2TcpSettingsUiState(val port: Constraint<Port> = Constraint.Any) diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VpnSettingItem.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VpnSettingItem.kt index 6f18260f5d..1016b62af8 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VpnSettingItem.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VpnSettingItem.kt @@ -5,7 +5,6 @@ import net.mullvad.mullvadvpn.lib.model.IpVersion import net.mullvad.mullvadvpn.lib.model.Port import net.mullvad.mullvadvpn.lib.model.PortRange import net.mullvad.mullvadvpn.lib.model.QuantumResistantState -import net.mullvad.mullvadvpn.viewmodel.CustomDnsItem sealed interface VpnSettingItem { diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VpnSettingsUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VpnSettingsUiState.kt new file mode 100644 index 0000000000..1526171b1b --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VpnSettingsUiState.kt @@ -0,0 +1,251 @@ +package net.mullvad.mullvadvpn.compose.state + +import net.mullvad.mullvadvpn.constant.WIREGUARD_PRESET_PORTS +import net.mullvad.mullvadvpn.lib.model.Constraint +import net.mullvad.mullvadvpn.lib.model.DefaultDnsOptions +import net.mullvad.mullvadvpn.lib.model.IpVersion +import net.mullvad.mullvadvpn.lib.model.Mtu +import net.mullvad.mullvadvpn.lib.model.ObfuscationMode +import net.mullvad.mullvadvpn.lib.model.Port +import net.mullvad.mullvadvpn.lib.model.PortRange +import net.mullvad.mullvadvpn.lib.model.QuantumResistantState + +data class VpnSettingsUiState(val settings: List<VpnSettingItem>, val isModal: Boolean) { + + companion object { + @Suppress("LongParameterList", "CyclomaticComplexMethod", "LongMethod") + fun from( + mtu: Mtu?, + isLocalNetworkSharingEnabled: Boolean, + isCustomDnsEnabled: Boolean, + customDnsItems: List<CustomDnsItem>, + contentBlockersOptions: DefaultDnsOptions, + obfuscationMode: ObfuscationMode, + selectedUdp2TcpObfuscationPort: Constraint<Port>, + selectedShadowsocksObfuscationPort: Constraint<Port>, + quantumResistant: QuantumResistantState, + selectedWireguardPort: Constraint<Port>, + customWireguardPort: Port?, + availablePortRanges: List<PortRange>, + systemVpnSettingsAvailable: Boolean, + autoStartAndConnectOnBoot: Boolean, + deviceIpVersion: Constraint<IpVersion>, + isIpv6Enabled: Boolean, + isContentBlockersExpanded: Boolean, + isModal: Boolean, + ) = + VpnSettingsUiState( + buildList { + if (systemVpnSettingsAvailable) { + add(VpnSettingItem.AutoConnectAndLockdownMode) + add(VpnSettingItem.AutoConnectAndLockdownModeInfo) + } else { + add(VpnSettingItem.ConnectDeviceOnStartUpSetting(autoStartAndConnectOnBoot)) + add(VpnSettingItem.ConnectDeviceOnStartUpInfo) + } + + // Local network sharing + add(VpnSettingItem.LocalNetworkSharingSetting(isLocalNetworkSharingEnabled)) + add(VpnSettingItem.Spacer) + + // Dns Content Blockers + add( + VpnSettingItem.DnsContentBlockersHeader( + !isCustomDnsEnabled, + isContentBlockersExpanded, + ) + ) + add(VpnSettingItem.Divider) + + if (isContentBlockersExpanded) { + with(contentBlockersOptions) { + add( + VpnSettingItem.DnsContentBlockerItem.Ads( + blockAds, + !isCustomDnsEnabled, + ) + ) + add(VpnSettingItem.Divider) + add( + VpnSettingItem.DnsContentBlockerItem.Trackers( + blockTrackers, + !isCustomDnsEnabled, + ) + ) + add(VpnSettingItem.Divider) + add( + VpnSettingItem.DnsContentBlockerItem.Malware( + blockMalware, + !isCustomDnsEnabled, + ) + ) + add(VpnSettingItem.Divider) + add( + VpnSettingItem.DnsContentBlockerItem.Gambling( + blockGambling, + !isCustomDnsEnabled, + ) + ) + add(VpnSettingItem.Divider) + add( + VpnSettingItem.DnsContentBlockerItem.AdultContent( + blockAdultContent, + !isCustomDnsEnabled, + ) + ) + add(VpnSettingItem.Divider) + add( + VpnSettingItem.DnsContentBlockerItem.SocialMedia( + blockSocialMedia, + !isCustomDnsEnabled, + ) + ) + } + if (isCustomDnsEnabled) { + add(VpnSettingItem.DnsContentBlockersUnavailable) + } + } + + // Custom DNS + add( + VpnSettingItem.CustomDnsServerSetting( + isCustomDnsEnabled, + !contentBlockersOptions.isAnyBlockerEnabled(), + ) + ) + if (isCustomDnsEnabled) { + customDnsItems.forEachIndexed { index, item -> + add( + VpnSettingItem.CustomDnsEntry( + index, + item, + showUnreachableLocalDnsWarning = + item.isLocal && !isLocalNetworkSharingEnabled, + showUnreachableIpv6DnsWarning = item.isIpv6 && !isIpv6Enabled, + ) + ) + add(VpnSettingItem.Divider) + } + if (customDnsItems.isNotEmpty()) { + add(VpnSettingItem.CustomDnsAdd) + } + } + + if (contentBlockersOptions.isAnyBlockerEnabled()) { + add(VpnSettingItem.CustomDnsUnavailable) + } else if (customDnsItems.isEmpty()) { + add(VpnSettingItem.CustomDnsInfo) + } else { + add(VpnSettingItem.Spacer) + } + + // IPv6 + add(VpnSettingItem.EnableIpv6Setting(isIpv6Enabled)) + + add(VpnSettingItem.Spacer) + + // Wireguard Port + val isWireguardPortEnabled = + obfuscationMode == ObfuscationMode.Auto || + obfuscationMode == ObfuscationMode.Off + add( + VpnSettingItem.WireguardPortHeader( + isWireguardPortEnabled, + availablePortRanges, + ) + ) + (listOf(Constraint.Any) + WIREGUARD_PRESET_PORTS.map { Constraint.Only(it) }) + .forEach { + add(VpnSettingItem.Divider) + add( + VpnSettingItem.WireguardPortItem.Constraint( + isWireguardPortEnabled, + it == selectedWireguardPort, + it, + ) + ) + } + add(VpnSettingItem.Divider) + add( + VpnSettingItem.WireguardPortItem.WireguardPortCustom( + isWireguardPortEnabled, + selectedWireguardPort is Constraint.Only && + selectedWireguardPort.value == customWireguardPort, + customWireguardPort, + availablePortRanges, + ) + ) + + if (!isWireguardPortEnabled) { + add(VpnSettingItem.WireguardPortUnavailable) + } else { + add(VpnSettingItem.Spacer) + } + + // Wireguard Obfuscation + add(VpnSettingItem.ObfuscationHeader) + add(VpnSettingItem.Divider) + add( + VpnSettingItem.ObfuscationItem.Automatic( + obfuscationMode == ObfuscationMode.Auto + ) + ) + add(VpnSettingItem.Divider) + add( + VpnSettingItem.ObfuscationItem.Shadowsocks( + obfuscationMode == ObfuscationMode.Shadowsocks, + selectedShadowsocksObfuscationPort, + ) + ) + add(VpnSettingItem.Divider) + add( + VpnSettingItem.ObfuscationItem.UdpOverTcp( + obfuscationMode == ObfuscationMode.Udp2Tcp, + selectedUdp2TcpObfuscationPort, + ) + ) + add(VpnSettingItem.Divider) + add(VpnSettingItem.ObfuscationItem.Off(obfuscationMode == ObfuscationMode.Off)) + + add(VpnSettingItem.Spacer) + + // Quantum Resistance + add(VpnSettingItem.QuantumResistanceHeader) + QuantumResistantState.entries.forEach { + add(VpnSettingItem.Divider) + add(VpnSettingItem.QuantumItem(it, quantumResistant == it)) + } + + add(VpnSettingItem.Spacer) + + // Device Ip Version + add(VpnSettingItem.DeviceIpVersionHeader) + + IpVersion.constraints.forEach { + add(VpnSettingItem.Divider) + add(VpnSettingItem.DeviceIpVersionItem(it, deviceIpVersion == it)) + } + + add(VpnSettingItem.Spacer) + + // MTU + add(VpnSettingItem.Mtu(mtu)) + add(VpnSettingItem.MtuInfo) + + add(VpnSettingItem.ServerIpOverrides) + add(VpnSettingItem.Spacer) + }, + isModal = isModal, + ) + } +} + +data class CustomDnsItem(val address: String, val isLocal: Boolean, val isIpv6: Boolean) { + companion object { + private const val EMPTY_STRING = "" + + fun default(): CustomDnsItem { + return CustomDnsItem(address = EMPTY_STRING, isLocal = false, isIpv6 = false) + } + } +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt index f7b41b9378..95e2b4c7e7 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt @@ -189,7 +189,6 @@ val uiModule = module { viewModel { ChangelogViewModel(get(), get(), get()) } viewModel { AppInfoViewModel( - changelogRepository = get(), appVersionInfoRepository = get(), resources = get(), isPlayBuild = IS_PLAY_BUILD, @@ -248,7 +247,7 @@ val uiModule = module { viewModel { ApiAccessMethodDetailsViewModel(get(), get()) } viewModel { DeleteApiAccessMethodConfirmationViewModel(get(), get()) } viewModel { Udp2TcpSettingsViewModel(get()) } - viewModel { ShadowsocksSettingsViewModel(get(), get()) } + viewModel { ShadowsocksSettingsViewModel(get()) } viewModel { ShadowsocksCustomPortDialogViewModel(get()) } viewModel { MultihopViewModel(get(), get()) } viewModel { diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AppInfoViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AppInfoViewModel.kt index c4c3be4b85..db5f499479 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AppInfoViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AppInfoViewModel.kt @@ -8,18 +8,16 @@ import androidx.lifecycle.viewModelScope import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.lib.model.VersionInfo -import net.mullvad.mullvadvpn.repository.ChangelogRepository import net.mullvad.mullvadvpn.ui.serviceconnection.AppVersionInfoRepository +import net.mullvad.mullvadvpn.util.Lc class AppInfoViewModel( - changelogRepository: ChangelogRepository, appVersionInfoRepository: AppVersionInfoRepository, private val resources: Resources, private val isPlayBuild: Boolean, @@ -30,19 +28,10 @@ class AppInfoViewModel( private val _uiSideEffect = Channel<AppInfoSideEffect>() val uiSideEffect = _uiSideEffect.receiveAsFlow() - val uiState: StateFlow<AppInfoUiState> = - combine( - appVersionInfoRepository.versionInfo, - flowOf(changelogRepository.getLastVersionChanges()), - flowOf(isPlayBuild), - ) { versionInfo, changes, isPlayBuild -> - AppInfoUiState(versionInfo, isPlayBuild) - } - .stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(), - AppInfoUiState(appVersionInfoRepository.versionInfo.value, true), - ) + val uiState: StateFlow<Lc<Unit, AppInfoUiState>> = + appVersionInfoRepository.versionInfo + .map { versionInfo -> Lc.Content(AppInfoUiState(versionInfo, isPlayBuild)) } + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), Lc.Loading(Unit)) fun openAppListing() = viewModelScope.launch { diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DaitaViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DaitaViewModel.kt index c6caeb6973..f941b26455 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DaitaViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DaitaViewModel.kt @@ -5,12 +5,15 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.ramcosta.composedestinations.generated.destinations.DaitaDestination import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import net.mullvad.mullvadvpn.compose.state.DaitaUiState import net.mullvad.mullvadvpn.lib.model.Settings import net.mullvad.mullvadvpn.repository.SettingsRepository +import net.mullvad.mullvadvpn.util.Lc +import net.mullvad.mullvadvpn.util.toLc class DaitaViewModel( private val settingsRepository: SettingsRepository, @@ -21,17 +24,19 @@ class DaitaViewModel( val uiState = settingsRepository.settingsUpdates + .filterNotNull() .map { settings -> DaitaUiState( - daitaEnabled = settings?.daitaSettings()?.enabled == true, - directOnly = settings?.daitaSettings()?.directOnly == true, - navArgs.isModal, - ) + daitaEnabled = settings.daitaSettings().enabled, + directOnly = settings.daitaSettings().directOnly, + navArgs.isModal, + ) + .toLc<Boolean, DaitaUiState>() } .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(), - initialValue = DaitaUiState(daitaEnabled = false, directOnly = false), + initialValue = Lc.Loading(navArgs.isModal), ) fun setDaita(enable: Boolean) { diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/MultihopViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/MultihopViewModel.kt index 278d0ab2e6..7b5b08a088 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/MultihopViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/MultihopViewModel.kt @@ -6,10 +6,12 @@ import androidx.lifecycle.viewModelScope import com.ramcosta.composedestinations.generated.destinations.MultihopDestination import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import net.mullvad.mullvadvpn.repository.WireguardConstraintsRepository +import net.mullvad.mullvadvpn.util.Lc class MultihopViewModel( private val wireguardConstraintsRepository: WireguardConstraintsRepository, @@ -17,10 +19,11 @@ class MultihopViewModel( ) : ViewModel() { private val navArgs = MultihopDestination.argsFrom(savedStateHandle) - val uiState: StateFlow<MultihopUiState> = + val uiState: StateFlow<Lc<Boolean, MultihopUiState>> = wireguardConstraintsRepository.wireguardConstraints - .map { MultihopUiState(it?.isMultihopEnabled ?: false, isModal = navArgs.isModal) } - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), MultihopUiState(false)) + .filterNotNull() + .map { Lc.Content(MultihopUiState(it.isMultihopEnabled, isModal = navArgs.isModal)) } + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), Lc.Loading(navArgs.isModal)) fun setMultihop(enable: Boolean) { viewModelScope.launch { wireguardConstraintsRepository.setMultihop(enable) } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ServerIpOverridesViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ServerIpOverridesViewModel.kt index 16da4d23e5..9d9d0380b3 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ServerIpOverridesViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ServerIpOverridesViewModel.kt @@ -18,6 +18,8 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import net.mullvad.mullvadvpn.lib.model.SettingsPatchError import net.mullvad.mullvadvpn.repository.RelayOverridesRepository +import net.mullvad.mullvadvpn.util.Lc +import net.mullvad.mullvadvpn.util.toLc class ServerIpOverridesViewModel( private val relayOverridesRepository: RelayOverridesRepository, @@ -29,20 +31,17 @@ class ServerIpOverridesViewModel( private val _uiSideEffect = Channel<ServerIpOverridesUiSideEffect>() val uiSideEffect = merge(_uiSideEffect.receiveAsFlow()) - val uiState: StateFlow<ServerIpOverridesUiState> = + val uiState: StateFlow<Lc<Boolean, ServerIpOverridesUiState>> = relayOverridesRepository.relayOverrides .filterNotNull() .map { - ServerIpOverridesUiState.Loaded( - overridesActive = it.isNotEmpty(), - isModal = navArgs.isModal, - ) + ServerIpOverridesUiState( + overridesActive = it.isNotEmpty(), + isModal = navArgs.isModal, + ) + .toLc<Boolean, ServerIpOverridesUiState>() } - .stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(), - ServerIpOverridesUiState.Loading(navArgs.isModal), - ) + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), Lc.Loading(navArgs.isModal)) fun importFile(uri: Uri) = viewModelScope.launch { @@ -55,7 +54,7 @@ class ServerIpOverridesViewModel( fun importText(json: String) = viewModelScope.launch { applySettingsPatch(json) } - private suspend fun applySettingsPatch(json: String) { + private fun applySettingsPatch(json: String) { // Since we are currently using waitForReady this will just wait to apply until gRPC is // ready viewModelScope.launch { @@ -75,16 +74,4 @@ sealed interface ServerIpOverridesUiSideEffect { data class ImportResult(val error: SettingsPatchError?) : ServerIpOverridesUiSideEffect } -sealed interface ServerIpOverridesUiState { - val overridesActive: Boolean? - get() = (this as? Loaded)?.overridesActive - - val isModal: Boolean - - data class Loading(override val isModal: Boolean = false) : ServerIpOverridesUiState - - data class Loaded( - override val overridesActive: Boolean, - override val isModal: Boolean = false, - ) : ServerIpOverridesUiState -} +data class ServerIpOverridesUiState(val overridesActive: Boolean, val isModal: Boolean = false) diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModel.kt index 5cc6f1562b..b3b09889c3 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModel.kt @@ -12,6 +12,8 @@ import net.mullvad.mullvadvpn.lib.shared.DeviceRepository import net.mullvad.mullvadvpn.repository.SettingsRepository import net.mullvad.mullvadvpn.repository.WireguardConstraintsRepository import net.mullvad.mullvadvpn.ui.serviceconnection.AppVersionInfoRepository +import net.mullvad.mullvadvpn.util.Lc +import net.mullvad.mullvadvpn.util.toLc class SettingsViewModel( deviceRepository: DeviceRepository, @@ -21,7 +23,7 @@ class SettingsViewModel( isPlayBuild: Boolean, ) : ViewModel() { - val uiState: StateFlow<SettingsUiState> = + val uiState: StateFlow<Lc<Unit, SettingsUiState>> = combine( deviceRepository.deviceState, appVersionInfoRepository.versionInfo, @@ -29,25 +31,15 @@ class SettingsViewModel( settingsRepository.settingsUpdates, ) { deviceState, versionInfo, wireguardConstraints, settings -> SettingsUiState( - isLoggedIn = deviceState is DeviceState.LoggedIn, - appVersion = versionInfo.currentVersion, - isSupportedVersion = versionInfo.isSupported, - multihopEnabled = wireguardConstraints?.isMultihopEnabled == true, - isDaitaEnabled = - settings?.tunnelOptions?.wireguard?.daitaSettings?.enabled == true, - isPlayBuild = isPlayBuild, - ) + isLoggedIn = deviceState is DeviceState.LoggedIn, + appVersion = versionInfo.currentVersion, + isSupportedVersion = versionInfo.isSupported, + multihopEnabled = wireguardConstraints?.isMultihopEnabled == true, + isDaitaEnabled = + settings?.tunnelOptions?.wireguard?.daitaSettings?.enabled == true, + isPlayBuild = isPlayBuild, + ) + .toLc<Unit, SettingsUiState>() } - .stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(), - SettingsUiState( - appVersion = "", - isLoggedIn = false, - isSupportedVersion = true, - isDaitaEnabled = false, - isPlayBuild = isPlayBuild, - multihopEnabled = false, - ), - ) + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), Lc.Loading(Unit)) } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ShadowsocksSettingsViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ShadowsocksSettingsViewModel.kt index 18197e2e42..fa0e886fee 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ShadowsocksSettingsViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ShadowsocksSettingsViewModel.kt @@ -12,37 +12,34 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import net.mullvad.mullvadvpn.compose.state.ShadowsocksSettingsState +import net.mullvad.mullvadvpn.compose.state.ShadowsocksSettingsUiState import net.mullvad.mullvadvpn.constant.SHADOWSOCKS_PRESET_PORTS import net.mullvad.mullvadvpn.lib.model.Constraint import net.mullvad.mullvadvpn.lib.model.Port import net.mullvad.mullvadvpn.lib.model.Settings -import net.mullvad.mullvadvpn.repository.RelayListRepository import net.mullvad.mullvadvpn.repository.SettingsRepository +import net.mullvad.mullvadvpn.util.Lc +import net.mullvad.mullvadvpn.util.toLc -class ShadowsocksSettingsViewModel( - private val settingsRepository: SettingsRepository, - relayListRepository: RelayListRepository, -) : ViewModel() { +class ShadowsocksSettingsViewModel(private val settingsRepository: SettingsRepository) : + ViewModel() { private val customPort = MutableStateFlow<Port?>(null) - val uiState: StateFlow<ShadowsocksSettingsState> = - combine( - settingsRepository.settingsUpdates.filterNotNull(), - customPort, - relayListRepository.shadowsocksPortRanges, - ) { settings, customPort, portRanges -> - ShadowsocksSettingsState( - port = settings.getShadowSocksPort(), - customPort = customPort, - validPortRanges = portRanges, - ) + val uiState: StateFlow<Lc<Unit, ShadowsocksSettingsUiState>> = + combine(settingsRepository.settingsUpdates.filterNotNull(), customPort) { + settings, + customPort -> + ShadowsocksSettingsUiState( + port = settings.getShadowSocksPort(), + customPort = customPort, + ) + .toLc<Unit, ShadowsocksSettingsUiState>() } .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(), - initialValue = ShadowsocksSettingsState(), + initialValue = Lc.Loading(Unit), ) init { @@ -73,7 +70,7 @@ class ShadowsocksSettingsViewModel( } fun resetCustomPort() { - val isCustom = uiState.value.isCustom + val isCustom = uiState.value.contentOrNull()?.isCustom == true customPort.update { null } // If custom port was selected, update selection to be any. if (isCustom) { diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModel.kt index 07c0383480..8743afc308 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModel.kt @@ -14,9 +14,9 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import net.mullvad.mullvadvpn.applist.AppData import net.mullvad.mullvadvpn.applist.ApplicationsProvider -import net.mullvad.mullvadvpn.compose.state.SplitTunnelingUiState import net.mullvad.mullvadvpn.lib.model.AppId import net.mullvad.mullvadvpn.repository.SplitTunnelingRepository +import net.mullvad.mullvadvpn.util.Lc class SplitTunnelingViewModel( private val appsProvider: ApplicationsProvider, @@ -55,7 +55,7 @@ class SplitTunnelingViewModel( .stateIn( viewModelScope, SharingStarted.WhileSubscribed(), - SplitTunnelingUiState.Loading(enabled = false, isModal = navArgs.isModal), + Lc.Loading(Loading(enabled = false, isModal = navArgs.isModal)), ) init { @@ -88,3 +88,13 @@ class SplitTunnelingViewModel( appsProvider.getAppsList().let { appsList -> allApps.emit(appsList) } } } + +data class Loading(val enabled: Boolean = false, val isModal: Boolean = false) + +data class SplitTunnelingUiState( + val enabled: Boolean = false, + val excludedApps: List<AppData> = emptyList(), + val includedApps: List<AppData> = emptyList(), + val showSystemApps: Boolean = false, + val isModal: Boolean = false, +) diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelState.kt index 1f23b5f4b9..67011fbeb1 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelState.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelState.kt @@ -1,8 +1,9 @@ package net.mullvad.mullvadvpn.viewmodel import net.mullvad.mullvadvpn.applist.AppData -import net.mullvad.mullvadvpn.compose.state.SplitTunnelingUiState import net.mullvad.mullvadvpn.lib.model.AppId +import net.mullvad.mullvadvpn.util.Lc +import net.mullvad.mullvadvpn.util.toLc data class SplitTunnelingViewModelState( val enabled: Boolean = false, @@ -10,7 +11,7 @@ data class SplitTunnelingViewModelState( val allApps: List<AppData>? = null, val showSystemApps: Boolean = false, ) { - fun toUiState(isModal: Boolean): SplitTunnelingUiState { + fun toUiState(isModal: Boolean): Lc<Loading, SplitTunnelingUiState> { return allApps ?.partition { appData -> if (enabled) { @@ -20,20 +21,21 @@ data class SplitTunnelingViewModelState( } } ?.let { (excluded, included) -> - SplitTunnelingUiState.ShowAppList( - enabled = enabled, - excludedApps = excluded.sortedWith(descendingByNameComparator), - includedApps = - if (showSystemApps) { - included - } else { - included.filter { appData -> !appData.isSystemApp } - } - .sortedWith(descendingByNameComparator), - showSystemApps = showSystemApps, - isModal = isModal, - ) - } ?: SplitTunnelingUiState.Loading(enabled = enabled, isModal) + SplitTunnelingUiState( + enabled = enabled, + excludedApps = excluded.sortedWith(descendingByNameComparator), + includedApps = + if (showSystemApps) { + included + } else { + included.filter { appData -> !appData.isSystemApp } + } + .sortedWith(descendingByNameComparator), + showSystemApps = showSystemApps, + isModal = isModal, + ) + .toLc() + } ?: Lc.Loading(Loading(enabled = enabled, isModal)) } companion object { diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/Udp2TcpSettingsViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/Udp2TcpSettingsViewModel.kt index bafe3ff76a..0d7d1293b5 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/Udp2TcpSettingsViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/Udp2TcpSettingsViewModel.kt @@ -9,22 +9,25 @@ import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import net.mullvad.mullvadvpn.compose.state.Udp2TcpSettingsState +import net.mullvad.mullvadvpn.compose.state.Udp2TcpSettingsUiState import net.mullvad.mullvadvpn.lib.model.Constraint import net.mullvad.mullvadvpn.lib.model.Port import net.mullvad.mullvadvpn.repository.SettingsRepository +import net.mullvad.mullvadvpn.util.Lc +import net.mullvad.mullvadvpn.util.toLc class Udp2TcpSettingsViewModel(private val repository: SettingsRepository) : ViewModel() { - val uiState: StateFlow<Udp2TcpSettingsState> = + val uiState: StateFlow<Lc<Unit, Udp2TcpSettingsUiState>> = repository.settingsUpdates .filterNotNull() .map { settings -> - Udp2TcpSettingsState(port = settings.obfuscationSettings.udp2tcp.port) + Udp2TcpSettingsUiState(port = settings.obfuscationSettings.udp2tcp.port) + .toLc<Unit, Udp2TcpSettingsUiState>() } .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(), - initialValue = Udp2TcpSettingsState(), + initialValue = Lc.Loading(Unit), ) fun onObfuscationPortSelected(port: Constraint<Port>) { diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModel.kt index 7a4dce5820..3f9a727b38 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModel.kt @@ -23,6 +23,8 @@ import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import net.mullvad.mullvadvpn.compose.state.CustomDnsItem +import net.mullvad.mullvadvpn.compose.state.VpnSettingsUiState import net.mullvad.mullvadvpn.constant.WIREGUARD_PRESET_PORTS import net.mullvad.mullvadvpn.lib.model.Constraint import net.mullvad.mullvadvpn.lib.model.DefaultDnsOptions @@ -37,7 +39,9 @@ import net.mullvad.mullvadvpn.repository.RelayListRepository import net.mullvad.mullvadvpn.repository.SettingsRepository import net.mullvad.mullvadvpn.repository.WireguardConstraintsRepository import net.mullvad.mullvadvpn.usecase.SystemVpnSettingsAvailableUseCase +import net.mullvad.mullvadvpn.util.Lc import net.mullvad.mullvadvpn.util.onFirst +import net.mullvad.mullvadvpn.util.toLc sealed interface VpnSettingsSideEffect { sealed interface ShowToast : VpnSettingsSideEffect { @@ -93,33 +97,30 @@ class VpnSettingsViewModel( customWgPort, autoStartAndConnectOnBoot, isContentBlockersExpanded -> - VpnSettingsUiState.Content.from( - mtu = settings.tunnelOptions.wireguard.mtu, - isLocalNetworkSharingEnabled = settings.allowLan, - isCustomDnsEnabled = settings.isCustomDnsEnabled(), - customDnsItems = settings.addresses().asStringAddressList(), - contentBlockersOptions = settings.contentBlockersSettings(), - obfuscationMode = settings.selectedObfuscationMode(), - selectedUdp2TcpObfuscationPort = settings.obfuscationSettings.udp2tcp.port, - selectedShadowsocksObfuscationPort = - settings.obfuscationSettings.shadowsocks.port, - quantumResistant = settings.quantumResistant(), - selectedWireguardPort = settings.getWireguardPort(), - customWireguardPort = customWgPort, - availablePortRanges = portRanges, - systemVpnSettingsAvailable = systemVpnSettingsUseCase(), - autoStartAndConnectOnBoot = autoStartAndConnectOnBoot, - deviceIpVersion = settings.getDeviceIpVersion(), - isIpv6Enabled = settings.tunnelOptions.genericOptions.enableIpv6, - isContentBlockersExpanded = isContentBlockersExpanded, - isModal = navArgs.isModal, - ) + VpnSettingsUiState.from( + mtu = settings.tunnelOptions.wireguard.mtu, + isLocalNetworkSharingEnabled = settings.allowLan, + isCustomDnsEnabled = settings.isCustomDnsEnabled(), + customDnsItems = settings.addresses().asStringAddressList(), + contentBlockersOptions = settings.contentBlockersSettings(), + obfuscationMode = settings.selectedObfuscationMode(), + selectedUdp2TcpObfuscationPort = settings.obfuscationSettings.udp2tcp.port, + selectedShadowsocksObfuscationPort = + settings.obfuscationSettings.shadowsocks.port, + quantumResistant = settings.quantumResistant(), + selectedWireguardPort = settings.getWireguardPort(), + customWireguardPort = customWgPort, + availablePortRanges = portRanges, + systemVpnSettingsAvailable = systemVpnSettingsUseCase(), + autoStartAndConnectOnBoot = autoStartAndConnectOnBoot, + deviceIpVersion = settings.getDeviceIpVersion(), + isIpv6Enabled = settings.tunnelOptions.genericOptions.enableIpv6, + isContentBlockersExpanded = isContentBlockersExpanded, + isModal = navArgs.isModal, + ) + .toLc<Boolean, VpnSettingsUiState>() } - .stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(), - VpnSettingsUiState.Loading(navArgs.isModal), - ) + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), Lc.Loading(navArgs.isModal)) fun onToggleLocalNetworkSharing(isEnabled: Boolean) { viewModelScope.launch(dispatcher) { diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelState.kt deleted file mode 100644 index 21d051156b..0000000000 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelState.kt +++ /dev/null @@ -1,268 +0,0 @@ -package net.mullvad.mullvadvpn.viewmodel - -import net.mullvad.mullvadvpn.compose.state.VpnSettingItem -import net.mullvad.mullvadvpn.constant.WIREGUARD_PRESET_PORTS -import net.mullvad.mullvadvpn.lib.model.Constraint -import net.mullvad.mullvadvpn.lib.model.DefaultDnsOptions -import net.mullvad.mullvadvpn.lib.model.IpVersion -import net.mullvad.mullvadvpn.lib.model.Mtu -import net.mullvad.mullvadvpn.lib.model.ObfuscationMode -import net.mullvad.mullvadvpn.lib.model.Port -import net.mullvad.mullvadvpn.lib.model.PortRange -import net.mullvad.mullvadvpn.lib.model.QuantumResistantState - -sealed interface VpnSettingsUiState { - val isModal: Boolean - - data class Loading(override val isModal: Boolean = false) : VpnSettingsUiState - - data class Content(val settings: List<VpnSettingItem>, override val isModal: Boolean = false) : - VpnSettingsUiState { - companion object { - @Suppress("LongParameterList", "CyclomaticComplexMethod", "LongMethod") - fun from( - mtu: Mtu?, - isLocalNetworkSharingEnabled: Boolean, - isCustomDnsEnabled: Boolean, - customDnsItems: List<CustomDnsItem>, - contentBlockersOptions: DefaultDnsOptions, - obfuscationMode: ObfuscationMode, - selectedUdp2TcpObfuscationPort: Constraint<Port>, - selectedShadowsocksObfuscationPort: Constraint<Port>, - quantumResistant: QuantumResistantState, - selectedWireguardPort: Constraint<Port>, - customWireguardPort: Port?, - availablePortRanges: List<PortRange>, - systemVpnSettingsAvailable: Boolean, - autoStartAndConnectOnBoot: Boolean, - deviceIpVersion: Constraint<IpVersion>, - isIpv6Enabled: Boolean, - isContentBlockersExpanded: Boolean, - isModal: Boolean, - ) = - Content( - buildList { - if (systemVpnSettingsAvailable) { - add(VpnSettingItem.AutoConnectAndLockdownMode) - add(VpnSettingItem.AutoConnectAndLockdownModeInfo) - } else { - add( - VpnSettingItem.ConnectDeviceOnStartUpSetting( - autoStartAndConnectOnBoot - ) - ) - add(VpnSettingItem.ConnectDeviceOnStartUpInfo) - } - - // Local network sharing - add(VpnSettingItem.LocalNetworkSharingSetting(isLocalNetworkSharingEnabled)) - add(VpnSettingItem.Spacer) - - // Dns Content Blockers - add( - VpnSettingItem.DnsContentBlockersHeader( - !isCustomDnsEnabled, - isContentBlockersExpanded, - ) - ) - add(VpnSettingItem.Divider) - - if (isContentBlockersExpanded) { - with(contentBlockersOptions) { - add( - VpnSettingItem.DnsContentBlockerItem.Ads( - blockAds, - !isCustomDnsEnabled, - ) - ) - add(VpnSettingItem.Divider) - add( - VpnSettingItem.DnsContentBlockerItem.Trackers( - blockTrackers, - !isCustomDnsEnabled, - ) - ) - add(VpnSettingItem.Divider) - add( - VpnSettingItem.DnsContentBlockerItem.Malware( - blockMalware, - !isCustomDnsEnabled, - ) - ) - add(VpnSettingItem.Divider) - add( - VpnSettingItem.DnsContentBlockerItem.Gambling( - blockGambling, - !isCustomDnsEnabled, - ) - ) - add(VpnSettingItem.Divider) - add( - VpnSettingItem.DnsContentBlockerItem.AdultContent( - blockAdultContent, - !isCustomDnsEnabled, - ) - ) - add(VpnSettingItem.Divider) - add( - VpnSettingItem.DnsContentBlockerItem.SocialMedia( - blockSocialMedia, - !isCustomDnsEnabled, - ) - ) - } - if (isCustomDnsEnabled) { - add(VpnSettingItem.DnsContentBlockersUnavailable) - } - } - - // Custom DNS - add( - VpnSettingItem.CustomDnsServerSetting( - isCustomDnsEnabled, - !contentBlockersOptions.isAnyBlockerEnabled(), - ) - ) - if (isCustomDnsEnabled) { - customDnsItems.forEachIndexed { index, item -> - add( - VpnSettingItem.CustomDnsEntry( - index, - item, - showUnreachableLocalDnsWarning = - item.isLocal && !isLocalNetworkSharingEnabled, - showUnreachableIpv6DnsWarning = - item.isIpv6 && !isIpv6Enabled, - ) - ) - add(VpnSettingItem.Divider) - } - if (customDnsItems.isNotEmpty()) { - add(VpnSettingItem.CustomDnsAdd) - } - } - - if (contentBlockersOptions.isAnyBlockerEnabled()) { - add(VpnSettingItem.CustomDnsUnavailable) - } else if (customDnsItems.isEmpty()) { - add(VpnSettingItem.CustomDnsInfo) - } else { - add(VpnSettingItem.Spacer) - } - - // IPv6 - add(VpnSettingItem.EnableIpv6Setting(isIpv6Enabled)) - - add(VpnSettingItem.Spacer) - - // Wireguard Port - val isWireguardPortEnabled = - obfuscationMode == ObfuscationMode.Auto || - obfuscationMode == ObfuscationMode.Off - add( - VpnSettingItem.WireguardPortHeader( - isWireguardPortEnabled, - availablePortRanges, - ) - ) - (listOf(Constraint.Any) + - WIREGUARD_PRESET_PORTS.map { Constraint.Only(it) }) - .forEach { - add(VpnSettingItem.Divider) - add( - VpnSettingItem.WireguardPortItem.Constraint( - isWireguardPortEnabled, - it == selectedWireguardPort, - it, - ) - ) - } - add(VpnSettingItem.Divider) - add( - VpnSettingItem.WireguardPortItem.WireguardPortCustom( - isWireguardPortEnabled, - selectedWireguardPort is Constraint.Only && - selectedWireguardPort.value == customWireguardPort, - customWireguardPort, - availablePortRanges, - ) - ) - - if (!isWireguardPortEnabled) { - add(VpnSettingItem.WireguardPortUnavailable) - } else { - add(VpnSettingItem.Spacer) - } - - // Wireguard Obfuscation - add(VpnSettingItem.ObfuscationHeader) - add(VpnSettingItem.Divider) - add( - VpnSettingItem.ObfuscationItem.Automatic( - obfuscationMode == ObfuscationMode.Auto - ) - ) - add(VpnSettingItem.Divider) - add( - VpnSettingItem.ObfuscationItem.Shadowsocks( - obfuscationMode == ObfuscationMode.Shadowsocks, - selectedShadowsocksObfuscationPort, - ) - ) - add(VpnSettingItem.Divider) - add( - VpnSettingItem.ObfuscationItem.UdpOverTcp( - obfuscationMode == ObfuscationMode.Udp2Tcp, - selectedUdp2TcpObfuscationPort, - ) - ) - add(VpnSettingItem.Divider) - add( - VpnSettingItem.ObfuscationItem.Off( - obfuscationMode == ObfuscationMode.Off - ) - ) - - add(VpnSettingItem.Spacer) - - // Quantum Resistance - add(VpnSettingItem.QuantumResistanceHeader) - QuantumResistantState.entries.forEach { - add(VpnSettingItem.Divider) - add(VpnSettingItem.QuantumItem(it, quantumResistant == it)) - } - - add(VpnSettingItem.Spacer) - - // Device Ip Version - add(VpnSettingItem.DeviceIpVersionHeader) - - IpVersion.constraints.forEach { - add(VpnSettingItem.Divider) - add(VpnSettingItem.DeviceIpVersionItem(it, deviceIpVersion == it)) - } - - add(VpnSettingItem.Spacer) - - // MTU - add(VpnSettingItem.Mtu(mtu)) - add(VpnSettingItem.MtuInfo) - - add(VpnSettingItem.ServerIpOverrides) - add(VpnSettingItem.Spacer) - }, - isModal = isModal, - ) - } - } -} - -data class CustomDnsItem(val address: String, val isLocal: Boolean, val isIpv6: Boolean) { - companion object { - private const val EMPTY_STRING = "" - - fun default(): CustomDnsItem { - return CustomDnsItem(address = EMPTY_STRING, isLocal = false, isIpv6 = false) - } - } -} diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/DaitaViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/DaitaViewModelTest.kt index dcb06c76a7..e191192c15 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/DaitaViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/DaitaViewModelTest.kt @@ -7,6 +7,7 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.mockk +import kotlin.test.assertIs import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest import net.mullvad.mullvadvpn.compose.screen.DaitaNavArgs @@ -14,6 +15,7 @@ import net.mullvad.mullvadvpn.compose.state.DaitaUiState import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule import net.mullvad.mullvadvpn.lib.model.Settings import net.mullvad.mullvadvpn.repository.SettingsRepository +import net.mullvad.mullvadvpn.util.Lc import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -49,7 +51,11 @@ class DaitaViewModelTest { } // Act, Assert - viewModel.uiState.test { assertEquals(expectedState, awaitItem()) } + viewModel.uiState.test { + val item = awaitItem() + assertIs<Lc.Content<DaitaUiState>>(item) + assertEquals(expectedState, item.value) + } } @Test @@ -65,7 +71,11 @@ class DaitaViewModelTest { } // Act, Assert - viewModel.uiState.test { assertEquals(expectedState, awaitItem()) } + viewModel.uiState.test { + val item = awaitItem() + assertIs<Lc.Content<DaitaUiState>>(item) + assertEquals(expectedState, item.value) + } } @Test diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/MultihopViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/MultihopViewModelTest.kt index 6332666c22..d8c24c6d69 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/MultihopViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/MultihopViewModelTest.kt @@ -7,6 +7,7 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.mockk +import kotlin.test.assertIs import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest import net.mullvad.mullvadvpn.compose.screen.MultihopNavArgs @@ -14,6 +15,7 @@ import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule import net.mullvad.mullvadvpn.lib.model.Constraint import net.mullvad.mullvadvpn.lib.model.WireguardConstraints import net.mullvad.mullvadvpn.repository.WireguardConstraintsRepository +import net.mullvad.mullvadvpn.util.Lc import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -42,7 +44,7 @@ class MultihopViewModelTest { @Test fun `default state should be multihop disabled`() { - assertEquals(false, multihopViewModel.uiState.value.enable) + assertEquals(false, multihopViewModel.uiState.value.contentOrNull()?.enable == true) } @Test @@ -57,7 +59,11 @@ class MultihopViewModelTest { ) // Act, Assert - multihopViewModel.uiState.test { assertEquals(MultihopUiState(true), awaitItem()) } + multihopViewModel.uiState.test { + val item = awaitItem() + assertIs<Lc.Content<MultihopUiState>>(item) + assertEquals(MultihopUiState(true), item.value) + } } @Test diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ServerIpOverridesViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ServerIpOverridesViewModelTest.kt index d0d0a0a69c..683ab5640d 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ServerIpOverridesViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ServerIpOverridesViewModelTest.kt @@ -16,6 +16,7 @@ import io.mockk.unmockkAll import java.io.InputStream import java.io.InputStreamReader import kotlin.test.assertEquals +import kotlin.test.assertIs import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest @@ -24,6 +25,7 @@ import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule import net.mullvad.mullvadvpn.lib.model.RelayOverride import net.mullvad.mullvadvpn.lib.model.SettingsPatchError import net.mullvad.mullvadvpn.repository.RelayOverridesRepository +import net.mullvad.mullvadvpn.util.Lc import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -60,15 +62,17 @@ class ServerIpOverridesViewModelTest { @Test fun `ensure state is loading by default`() = runTest { - viewModel.uiState.test { assertEquals(ServerIpOverridesUiState.Loading(), awaitItem()) } + viewModel.uiState.test { assertIs<Lc.Loading<Unit>>(awaitItem()) } } @Test fun `when server ip overrides are empty ui state overrides should be inactive`() = runTest { viewModel.uiState.test { - assertEquals(ServerIpOverridesUiState.Loading(), awaitItem()) + assertIs<Lc.Loading<Unit>>(awaitItem()) relayOverrides.emit(emptyList()) - assertEquals(ServerIpOverridesUiState.Loaded(false), awaitItem()) + val item = awaitItem() + assertIs<Lc.Content<ServerIpOverridesUiState>>(item) + assertEquals(false, item.value.overridesActive) } } diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModelTest.kt index f0a60c50c2..0c86286be5 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModelTest.kt @@ -6,9 +6,11 @@ import io.mockk.every import io.mockk.mockk import io.mockk.unmockkAll import kotlin.test.assertEquals +import kotlin.test.assertIs import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest +import net.mullvad.mullvadvpn.compose.state.SettingsUiState import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule import net.mullvad.mullvadvpn.lib.model.Constraint import net.mullvad.mullvadvpn.lib.model.DeviceState @@ -19,6 +21,7 @@ import net.mullvad.mullvadvpn.lib.shared.DeviceRepository import net.mullvad.mullvadvpn.repository.SettingsRepository import net.mullvad.mullvadvpn.repository.WireguardConstraintsRepository import net.mullvad.mullvadvpn.ui.serviceconnection.AppVersionInfoRepository +import net.mullvad.mullvadvpn.util.Lc import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -68,7 +71,11 @@ class SettingsViewModelTest { @Test fun `uiState should return isLoggedIn false by default`() = runTest { // Act, Assert - viewModel.uiState.test { assertEquals(false, awaitItem().isLoggedIn) } + viewModel.uiState.test { + val item = awaitItem() + assertIs<Lc.Content<SettingsUiState>>(item) + assertEquals(false, item.value.isLoggedIn) + } } @Test @@ -81,7 +88,8 @@ class SettingsViewModelTest { // Act, Assert viewModel.uiState.test { val result = awaitItem() - assertEquals(true, result.isSupportedVersion) + assertIs<Lc.Content<SettingsUiState>>(result) + assertEquals(true, result.value.isSupportedVersion) } } @@ -95,7 +103,8 @@ class SettingsViewModelTest { // Act, Assert viewModel.uiState.test { val result = awaitItem() - assertEquals(false, result.isSupportedVersion) + assertIs<Lc.Content<SettingsUiState>>(result) + assertEquals(false, result.value.isSupportedVersion) } } @@ -114,7 +123,8 @@ class SettingsViewModelTest { // Act, Assert viewModel.uiState.test { val result = awaitItem() - assertEquals(true, result.multihopEnabled) + assertIs<Lc.Content<SettingsUiState>>(result) + assertEquals(true, result.value.multihopEnabled) } } } diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ShadowsocksSettingsViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ShadowsocksSettingsViewModelTest.kt index 5340914aaf..8068a1f1d2 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ShadowsocksSettingsViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ShadowsocksSettingsViewModelTest.kt @@ -6,17 +6,17 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.mockk +import kotlin.test.assertIs import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.test.runTest +import net.mullvad.mullvadvpn.compose.state.ShadowsocksSettingsUiState import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule -import net.mullvad.mullvadvpn.lib.common.test.assertLists import net.mullvad.mullvadvpn.lib.model.Constraint import net.mullvad.mullvadvpn.lib.model.Port -import net.mullvad.mullvadvpn.lib.model.PortRange import net.mullvad.mullvadvpn.lib.model.Settings -import net.mullvad.mullvadvpn.repository.RelayListRepository import net.mullvad.mullvadvpn.repository.SettingsRepository +import net.mullvad.mullvadvpn.util.Lc import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -26,23 +26,16 @@ import org.junit.jupiter.api.extension.ExtendWith class ShadowsocksSettingsViewModelTest { private val mockSettingsRepository: SettingsRepository = mockk() - private val mockRelayListRepository: RelayListRepository = mockk() private val settingsFlow = MutableStateFlow<Settings?>(null) - private val portRangesFlow = MutableStateFlow<List<PortRange>>(emptyList()) private lateinit var viewModel: ShadowsocksSettingsViewModel @BeforeEach fun setUp() { every { mockSettingsRepository.settingsUpdates } returns settingsFlow - every { mockRelayListRepository.shadowsocksPortRanges } returns portRangesFlow - viewModel = - ShadowsocksSettingsViewModel( - settingsRepository = mockSettingsRepository, - relayListRepository = mockRelayListRepository, - ) + viewModel = ShadowsocksSettingsViewModel(settingsRepository = mockSettingsRepository) } @Test @@ -57,27 +50,9 @@ class ShadowsocksSettingsViewModelTest { // Act, Assert viewModel.uiState.test { // Check result - val result = awaitItem().port - assertEquals(Constraint.Only(port), result) - } - } - - @Test - fun `uiState should reflect latest port range value from relay list`() = runTest { - // Arrange - val mockSettings: Settings = mockk() - val port = Port(123) - every { mockSettings.obfuscationSettings.shadowsocks.port } returns Constraint.Only(port) - val mockPortRange: List<PortRange> = listOf(mockk()) - - portRangesFlow.update { mockPortRange } - settingsFlow.update { mockSettings } - - // Act, Assert - viewModel.uiState.test { - // Check result - val result = awaitItem().validPortRanges - assertLists(mockPortRange, result) + val result = awaitItem() + assertIs<Lc.Content<ShadowsocksSettingsUiState>>(result) + assertEquals(Constraint.Only(port), result.value.port) } } @@ -110,12 +85,14 @@ class ShadowsocksSettingsViewModelTest { // Act, Assert viewModel.uiState.test { val startState = awaitItem() - assertEquals(port, startState.customPort) + assertIs<Lc.Content<ShadowsocksSettingsUiState>>(startState) + assertEquals(port, startState.value.customPort) viewModel.resetCustomPort() val updatedState = awaitItem() - assertEquals(null, updatedState.customPort) + assertIs<Lc.Content<ShadowsocksSettingsUiState>>(updatedState) + assertEquals(null, updatedState.value.customPort) coVerify { mockSettingsRepository.setCustomShadowsocksObfuscationPort(Constraint.Any) } } } diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelTest.kt index 1a4313ef6f..3d5a152b62 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelTest.kt @@ -12,6 +12,7 @@ import io.mockk.unmockkAll import io.mockk.verify import java.util.concurrent.TimeUnit import kotlin.test.assertEquals +import kotlin.test.assertIs import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.MutableStateFlow @@ -20,10 +21,10 @@ import kotlinx.coroutines.test.runTest import net.mullvad.mullvadvpn.applist.AppData import net.mullvad.mullvadvpn.applist.ApplicationsProvider import net.mullvad.mullvadvpn.compose.screen.SplitTunnelingNavArgs -import net.mullvad.mullvadvpn.compose.state.SplitTunnelingUiState import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule import net.mullvad.mullvadvpn.lib.model.AppId import net.mullvad.mullvadvpn.repository.SplitTunnelingRepository +import net.mullvad.mullvadvpn.util.Lc import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -57,10 +58,11 @@ class SplitTunnelingViewModelTest { @Test fun `initial state should be loading`() = runTest { initTestSubject(emptyList()) - val actualState: SplitTunnelingUiState = testSubject.uiState.value + val actualState: Lc<Loading, SplitTunnelingUiState> = testSubject.uiState.value - val initialExpectedState = SplitTunnelingUiState.Loading(enabled = false) + val initialExpectedState = Lc.Loading(Loading(enabled = false)) + assertIs<Lc.Loading<Loading>>(actualState) assertEquals(initialExpectedState, actualState) verify(exactly = 1) { mockedApplicationsProvider.getAppsList() } @@ -70,13 +72,17 @@ class SplitTunnelingViewModelTest { fun `empty app list should work`() = runTest { initTestSubject(emptyList()) val expectedState = - SplitTunnelingUiState.ShowAppList( + SplitTunnelingUiState( enabled = true, excludedApps = emptyList(), includedApps = emptyList(), showSystemApps = false, ) - testSubject.uiState.test { assertEquals(expectedState, awaitItem()) } + testSubject.uiState.test { + val item = awaitItem() + assertIs<Lc.Content<SplitTunnelingUiState>>(item) + assertEquals(expectedState, item.value) + } } @Test @@ -88,7 +94,7 @@ class SplitTunnelingViewModelTest { excludedApps.value = setOf(AppId(appExcluded.packageName)) val expectedState = - SplitTunnelingUiState.ShowAppList( + SplitTunnelingUiState( enabled = true, excludedApps = listOf(appExcluded), includedApps = listOf(appNotExcluded), @@ -97,7 +103,8 @@ class SplitTunnelingViewModelTest { testSubject.uiState.test { val actualState = awaitItem() - assertEquals(expectedState, actualState) + assertIs<Lc.Content<SplitTunnelingUiState>>(actualState) + assertEquals(expectedState, actualState.value) } } @@ -109,14 +116,14 @@ class SplitTunnelingViewModelTest { excludedApps.value = setOf(AppId(app.packageName)) val expectedStateBeforeAction = - SplitTunnelingUiState.ShowAppList( + SplitTunnelingUiState( enabled = true, excludedApps = listOf(app), includedApps = emptyList(), showSystemApps = false, ) val expectedStateAfterAction = - SplitTunnelingUiState.ShowAppList( + SplitTunnelingUiState( enabled = true, excludedApps = emptyList(), includedApps = listOf(app), @@ -126,10 +133,14 @@ class SplitTunnelingViewModelTest { Unit.right() testSubject.uiState.test { - assertEquals(expectedStateBeforeAction, awaitItem()) + val beforeAction = awaitItem() + assertIs<Lc.Content<SplitTunnelingUiState>>(beforeAction) + assertEquals(expectedStateBeforeAction, beforeAction.value) testSubject.onIncludeAppClick(app.packageName) excludedApps.value = emptySet() - assertEquals(expectedStateAfterAction, awaitItem()) + val afterAction = awaitItem() + assertIs<Lc.Content<SplitTunnelingUiState>>(afterAction) + assertEquals(expectedStateAfterAction, afterAction.value) coVerify { mockedSplitTunnelingRepository.includeApp(AppId(app.packageName)) } } @@ -142,7 +153,7 @@ class SplitTunnelingViewModelTest { initTestSubject(listOf(app)) val expectedStateBeforeAction = - SplitTunnelingUiState.ShowAppList( + SplitTunnelingUiState( enabled = true, excludedApps = emptyList(), includedApps = listOf(app), @@ -150,7 +161,7 @@ class SplitTunnelingViewModelTest { ) val expectedStateAfterAction = - SplitTunnelingUiState.ShowAppList( + SplitTunnelingUiState( enabled = true, excludedApps = listOf(app), includedApps = emptyList(), @@ -161,10 +172,14 @@ class SplitTunnelingViewModelTest { Unit.right() testSubject.uiState.test { - assertEquals(expectedStateBeforeAction, awaitItem()) + val beforeAction = awaitItem() + assertIs<Lc.Content<SplitTunnelingUiState>>(beforeAction) + assertEquals(expectedStateBeforeAction, beforeAction.value) testSubject.onExcludeAppClick(app.packageName) excludedApps.value = setOf(AppId(app.packageName)) - assertEquals(expectedStateAfterAction, awaitItem()) + val afterAction = awaitItem() + assertIs<Lc.Content<SplitTunnelingUiState>>(afterAction) + assertEquals(expectedStateAfterAction, afterAction.value) coVerify { mockedSplitTunnelingRepository.excludeApp(AppId(app.packageName)) } } @@ -175,11 +190,12 @@ class SplitTunnelingViewModelTest { initTestSubject(emptyList()) enabled.value = false - val expectedState = SplitTunnelingUiState.ShowAppList(enabled = false) + val expectedState = SplitTunnelingUiState(enabled = false) testSubject.uiState.test { val actualState = awaitItem() - assertEquals(expectedState, actualState) + assertIs<Lc.Content<SplitTunnelingUiState>>(actualState) + assertEquals(expectedState, actualState.value) } } @@ -191,7 +207,7 @@ class SplitTunnelingViewModelTest { val app3 = AppData("com.example.app3", 0, "App Z") val appList = listOf(app2, app1, app3) val expectedState = - SplitTunnelingUiState.ShowAppList( + SplitTunnelingUiState( enabled = true, includedApps = listOf(app1, app2, app3), showSystemApps = false, @@ -199,7 +215,11 @@ class SplitTunnelingViewModelTest { initTestSubject(appList = appList) // Assert - testSubject.uiState.test { assertEquals(expectedState, awaitItem()) } + testSubject.uiState.test { + val actualState = awaitItem() + assertIs<Lc.Content<SplitTunnelingUiState>>(actualState) + assertEquals(expectedState, actualState.value) + } } private fun initTestSubject(appList: List<AppData>) { diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/Udp2TcpSettingsViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/Udp2TcpSettingsViewModelTest.kt index 05114cd4fa..5b635f13bc 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/Udp2TcpSettingsViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/Udp2TcpSettingsViewModelTest.kt @@ -6,14 +6,17 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.mockk +import kotlin.test.assertIs import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.test.runTest +import net.mullvad.mullvadvpn.compose.state.Udp2TcpSettingsUiState import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule import net.mullvad.mullvadvpn.lib.model.Constraint import net.mullvad.mullvadvpn.lib.model.Port import net.mullvad.mullvadvpn.lib.model.Settings import net.mullvad.mullvadvpn.repository.SettingsRepository +import net.mullvad.mullvadvpn.util.Lc import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -47,8 +50,9 @@ class Udp2TcpSettingsViewModelTest { // Act, Assert viewModel.uiState.test { // Check result - val result = awaitItem().port - assertEquals(Constraint.Only(port), result) + val result = awaitItem() + assertIs<Lc.Content<Udp2TcpSettingsUiState>>(result) + assertEquals(Constraint.Only(port), result.value.port) } } diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelTest.kt index a6e2e713e1..f22df41f83 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelTest.kt @@ -22,6 +22,7 @@ import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import net.mullvad.mullvadvpn.compose.screen.VpnSettingsNavArgs import net.mullvad.mullvadvpn.compose.state.VpnSettingItem +import net.mullvad.mullvadvpn.compose.state.VpnSettingsUiState import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule import net.mullvad.mullvadvpn.lib.model.Constraint import net.mullvad.mullvadvpn.lib.model.DaitaSettings @@ -47,6 +48,7 @@ import net.mullvad.mullvadvpn.repository.RelayListRepository import net.mullvad.mullvadvpn.repository.SettingsRepository import net.mullvad.mullvadvpn.repository.WireguardConstraintsRepository import net.mullvad.mullvadvpn.usecase.SystemVpnSettingsAvailableUseCase +import net.mullvad.mullvadvpn.util.Lc import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -98,7 +100,7 @@ class VpnSettingsViewModelTest { @Test fun `initial state should be loading`() = runTest { - viewModel.uiState.test { assertEquals(VpnSettingsUiState.Loading(), awaitItem()) } + viewModel.uiState.test { assertInstanceOf<Lc.Loading<Boolean>>(awaitItem()) } } @Test @@ -148,13 +150,13 @@ class VpnSettingsViewModelTest { Constraint.Any viewModel.uiState.test { - assertEquals(VpnSettingsUiState.Loading(), awaitItem()) + assertInstanceOf<Lc.Loading<Boolean>>(awaitItem()) mockSettingsUpdate.value = mockSettings val content = awaitItem() - assertInstanceOf<VpnSettingsUiState.Content>(content) + assertInstanceOf<Lc.Content<VpnSettingsUiState>>(content) assertTrue( - content.settings + content.value.settings .filterIsInstance<VpnSettingItem.QuantumItem>() .first { it.quantumResistantState == QuantumResistantState.On } .selected @@ -191,14 +193,14 @@ class VpnSettingsViewModelTest { // Act, Assert viewModel.uiState.test { - assertInstanceOf<VpnSettingsUiState.Loading>(awaitItem()) + assertInstanceOf<Lc.Loading<Boolean>>(awaitItem()) mockSettingsUpdate.value = mockSettings with(awaitItem()) { - assertInstanceOf<VpnSettingsUiState.Content>(this) + assertInstanceOf<Lc.Content<VpnSettingsUiState>>(this) val customPortSetting = - settings + value.settings .filterIsInstance< VpnSettingItem.WireguardPortItem.WireguardPortCustom >() @@ -243,12 +245,14 @@ class VpnSettingsViewModelTest { every { mockSystemVpnSettingsUseCase() } returns systemVpnSettingsAvailable viewModel.uiState.test { - assertInstanceOf<VpnSettingsUiState.Loading>(awaitItem()) + assertInstanceOf<Lc.Loading<Boolean>>(awaitItem()) mockSettingsUpdate.value = dummySettings val content = awaitItem() - assertInstanceOf<VpnSettingsUiState.Content>(content) - assertTrue(content.settings.any { it is VpnSettingItem.AutoConnectAndLockdownMode }) + assertInstanceOf<Lc.Content<VpnSettingsUiState>>(content) + assertTrue( + content.value.settings.any { it is VpnSettingItem.AutoConnectAndLockdownMode } + ) } } @@ -262,12 +266,14 @@ class VpnSettingsViewModelTest { // Assert viewModel.uiState.test { - assertInstanceOf<VpnSettingsUiState.Loading>(awaitItem()) + assertInstanceOf<Lc.Loading<Boolean>>(awaitItem()) mockSettingsUpdate.value = dummySettings val content = awaitItem() - assertInstanceOf<VpnSettingsUiState.Content>(content) - assertTrue(content.settings.any { it is VpnSettingItem.ConnectDeviceOnStartUpSetting }) + assertInstanceOf<Lc.Content<VpnSettingsUiState>>(content) + assertTrue( + content.value.settings.any { it is VpnSettingItem.ConnectDeviceOnStartUpSetting } + ) } } @@ -311,10 +317,10 @@ class VpnSettingsViewModelTest { awaitItem() mockSettingsUpdate.value = mockSettings val content = awaitItem() - assertInstanceOf<VpnSettingsUiState.Content>(content) + assertInstanceOf<Lc.Content<VpnSettingsUiState>>(content) assertEquals( ipVersion, - content.settings + content.value.settings .filterIsInstance<VpnSettingItem.DeviceIpVersionItem>() .first { it.selected } .constraint, |
