summaryrefslogtreecommitdiffhomepage
path: root/android
diff options
context:
space:
mode:
authorJonatan Rhodin <jonatan.rhodin@mullvad.net>2025-07-30 14:18:10 +0200
committerJonatan Rhodin <jonatan.rhodin@mullvad.net>2025-07-30 14:18:10 +0200
commit83a65f5b2568543a5ff09bc90c4fca43ae2d3b0c (patch)
tree426e1981e717f1129dfaf7d01d85e32e0e190440 /android
parent155fa3bc298babfac523e621427d8ca9ac442b97 (diff)
parent4d4677a8fc7ed2339d8c744f8bd93b48e3b90c29 (diff)
downloadmullvadvpn-83a65f5b2568543a5ff09bc90c4fca43ae2d3b0c.tar.xz
mullvadvpn-83a65f5b2568543a5ff09bc90c4fca43ae2d3b0c.zip
Merge branch 'migrate-settings-screens-and-viewmodels-to-new-lce-structure-droid-2088'
Diffstat (limited to 'android')
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ServerIpOverridesScreenTest.kt18
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/SettingsScreenTest.kt34
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ShadowsocksSettingsScreenTest.kt19
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/SplitTunnelingScreenTest.kt74
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/Udp2TcpSettingsScreenTest.kt8
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreenTest.kt184
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/AppInfoUiStatePreviewParameterProvider.kt21
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/DaitaUiStatePreviewParameterProvider.kt15
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/MultihopUiStatePreviewParameterProvider.kt15
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/ServerIpOverridesUiStatePreviewParameterProvider.kt10
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SettingsUiStatePreviewParameterProvider.kt36
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/ShadowsocksSettingsUiStatePreviewParameterProvider.kt18
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SplitTunnelingUiStatePreviewParameterProvider.kt56
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/Udp2TcpSettingsUiStatePreviewParameterProvider.kt18
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/VpnSettingsUiStatePreviewParameterProvider.kt68
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AppInfoScreen.kt30
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/DaitaScreen.kt83
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/MultihopScreen.kt80
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ServerIpOverridesScreen.kt23
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SettingsScreen.kt107
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ShadowsocksSettingsScreen.kt119
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SplitTunnelingScreen.kt35
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/Udp2TcpSettingsScreen.kt91
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreen.kt25
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/ShadowsocksSettingsUiState.kt (renamed from android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/ShadowsocksSettingsState.kt)4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SplitTunnelingUiState.kt21
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/Udp2TcpSettingsUiState.kt (renamed from android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/Udp2TcpSettingsState.kt)2
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VpnSettingItem.kt1
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VpnSettingsUiState.kt251
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt3
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AppInfoViewModel.kt23
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DaitaViewModel.kt15
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/MultihopViewModel.kt9
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ServerIpOverridesViewModel.kt35
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModel.kt34
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ShadowsocksSettingsViewModel.kt35
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModel.kt14
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelState.kt34
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/Udp2TcpSettingsViewModel.kt11
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModel.kt53
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelState.kt268
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/DaitaViewModelTest.kt14
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/MultihopViewModelTest.kt10
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ServerIpOverridesViewModelTest.kt10
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModelTest.kt18
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ShadowsocksSettingsViewModelTest.kt45
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelTest.kt58
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/Udp2TcpSettingsViewModelTest.kt8
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelTest.kt36
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,