summaryrefslogtreecommitdiffhomepage
path: root/android/app/src
diff options
context:
space:
mode:
authorJonatan Rhodin <jonatan.rhodin@mullvad.net>2025-05-13 15:49:39 +0200
committerJonatan Rhodin <jonatan.rhodin@mullvad.net>2025-05-13 15:49:39 +0200
commit2beec9cb917cc274c4326345ae10ff041c665db0 (patch)
treea1f8dfe32fd7cd097b39a8aaecf39dd5acdc7014 /android/app/src
parentce62828a21cb65f2b0d1710b7d92b4e55a518408 (diff)
parent95d302c41f11f2b8090c955c59eab3e616430ee7 (diff)
downloadmullvadvpn-2beec9cb917cc274c4326345ae10ff041c665db0.tar.xz
mullvadvpn-2beec9cb917cc274c4326345ae10ff041c665db0.zip
Merge branch 'add-error-message-for-when-a-configured-port-is-invalid-droid-1982'
Diffstat (limited to 'android/app/src')
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreenTest.kt2
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationBanner.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt9
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt2
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/TunnelStateNotificationUseCase.kt57
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/TunnelStateNotificationUseCaseTest.kt88
6 files changed, 154 insertions, 8 deletions
diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreenTest.kt
index 14409f1fb2..1bed735bb0 100644
--- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreenTest.kt
+++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreenTest.kt
@@ -72,6 +72,7 @@ class ConnectScreenTest {
onChangelogClick: () -> Unit = {},
onDismissChangelogClick: () -> Unit = {},
onNavigateToFeature: (FeatureIndicator) -> Unit = {},
+ onClickShowWireguardPortSettings: () -> Unit = {},
) {
setContentWithTheme {
ConnectScreen(
@@ -89,6 +90,7 @@ class ConnectScreenTest {
onChangelogClick = onChangelogClick,
onDismissChangelogClick = onDismissChangelogClick,
onNavigateToFeature = onNavigateToFeature,
+ onClickShowWireguardPortSettings = onClickShowWireguardPortSettings,
)
}
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationBanner.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationBanner.kt
index 9ff9ec5a00..1605b83cdb 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationBanner.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationBanner.kt
@@ -55,6 +55,7 @@ private fun PreviewNotificationBanner() {
onClickShowChangelog = {},
onClickDismissChangelog = {},
onClickDismissNewDevice = {},
+ onClickShowWireguardPortSettings = {},
)
Spacer(modifier = Modifier.size(16.dp))
}
@@ -72,6 +73,7 @@ fun NotificationBanner(
onClickShowChangelog: () -> Unit,
onClickDismissChangelog: () -> Unit,
onClickDismissNewDevice: () -> Unit,
+ onClickShowWireguardPortSettings: () -> Unit,
) {
if (isTv()) {
NotificationBannerTv(
@@ -83,6 +85,7 @@ fun NotificationBanner(
onClickShowChangelog = onClickShowChangelog,
onClickDismissChangelog = onClickDismissChangelog,
onClickDismissNewDevice = onClickDismissNewDevice,
+ onClickShowWireguardPortSettings = onClickShowWireguardPortSettings,
)
} else {
AnimatedNotificationBanner(
@@ -95,6 +98,7 @@ fun NotificationBanner(
onClickShowChangelog = onClickShowChangelog,
onClickDismissChangelog = onClickDismissChangelog,
onClickDismissNewDevice = onClickDismissNewDevice,
+ onClickShowWireguardPortSettings = onClickShowWireguardPortSettings,
)
}
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt
index 94db024b41..26c08e4841 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt
@@ -164,6 +164,7 @@ private fun PreviewAccountScreen(
{},
{},
{},
+ {},
)
}
}
@@ -304,12 +305,15 @@ fun Connect(
dropUnlessResumed { feature: FeatureIndicator ->
navigator.navigate(feature.destination())
},
+ onClickShowWireguardPortSettings =
+ dropUnlessResumed { navigator.navigate(VpnSettingsDestination()) },
)
}
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
+@Suppress("LongParameterList")
fun ConnectScreen(
state: ConnectUiState,
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
@@ -326,6 +330,7 @@ fun ConnectScreen(
onAccountClick: () -> Unit,
onDismissNewDeviceClick: () -> Unit,
onNavigateToFeature: (FeatureIndicator) -> Unit,
+ onClickShowWireguardPortSettings: () -> Unit,
) {
val contentFocusRequester = remember { FocusRequester() }
@@ -346,6 +351,7 @@ fun ConnectScreen(
onDismissChangelogClick,
onDismissNewDeviceClick,
onNavigateToFeature,
+ onClickShowWireguardPortSettings,
)
}
@@ -384,6 +390,7 @@ fun ConnectScreen(
}
@Composable
+@Suppress("LongParameterList")
private fun Content(
focusRequester: FocusRequester,
paddingValues: PaddingValues,
@@ -399,6 +406,7 @@ private fun Content(
onDismissChangelogClick: () -> Unit,
onDismissNewDeviceClick: () -> Unit,
onNavigateToFeature: (FeatureIndicator) -> Unit,
+ onClickShowWireguardPortSettings: () -> Unit,
) {
val screenHeight = LocalWindowInfo.current.containerSize.height.dp
val indicatorPercentOffset =
@@ -446,6 +454,7 @@ private fun Content(
onClickShowChangelog = onChangelogClick,
onClickDismissChangelog = onDismissChangelogClick,
onClickDismissNewDevice = onDismissNewDeviceClick,
+ onClickShowWireguardPortSettings = onClickShowWireguardPortSettings,
)
ConnectionCard(
state = state,
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 c44aedd890..c9c1f1e78a 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
@@ -153,7 +153,7 @@ val uiModule = module {
single { WireguardConstraintsRepository(get()) }
single { AccountExpiryInAppNotificationUseCase(get()) }
- single { TunnelStateNotificationUseCase(get()) }
+ single { TunnelStateNotificationUseCase(get(), get(), get()) }
single { VersionNotificationUseCase(get(), BuildConfig.ENABLE_IN_APP_VERSION_NOTIFICATIONS) }
single { NewDeviceNotificationUseCase(get(), get()) }
single { NewChangelogNotificationUseCase(get()) }
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/TunnelStateNotificationUseCase.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/TunnelStateNotificationUseCase.kt
index 85ea7cf11a..753f8c1eef 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/TunnelStateNotificationUseCase.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/TunnelStateNotificationUseCase.kt
@@ -1,18 +1,46 @@
package net.mullvad.mullvadvpn.usecase
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import net.mullvad.mullvadvpn.lib.model.ActionAfterDisconnect
+import net.mullvad.mullvadvpn.lib.model.Constraint
+import net.mullvad.mullvadvpn.lib.model.ErrorState
+import net.mullvad.mullvadvpn.lib.model.ErrorStateCause
import net.mullvad.mullvadvpn.lib.model.InAppNotification
+import net.mullvad.mullvadvpn.lib.model.ParameterGenerationError
+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.lib.model.TunnelState
import net.mullvad.mullvadvpn.lib.shared.ConnectionProxy
+import net.mullvad.mullvadvpn.repository.RelayListRepository
+import net.mullvad.mullvadvpn.repository.SettingsRepository
+import net.mullvad.mullvadvpn.util.inAnyOf
-class TunnelStateNotificationUseCase(private val connectionProxy: ConnectionProxy) {
+class TunnelStateNotificationUseCase(
+ private val connectionProxy: ConnectionProxy,
+ private val relayListRepository: RelayListRepository,
+ private val settingsRepository: SettingsRepository,
+) {
+ @OptIn(ExperimentalCoroutinesApi::class)
operator fun invoke(): Flow<List<InAppNotification>> =
connectionProxy.tunnelState
.distinctUntilChanged()
.map(::tunnelStateNotification)
+ .flatMapLatest { inAppNotification ->
+ combine(relayListRepository.portRanges, settingsRepository.settingsUpdates) {
+ portRanges,
+ settings ->
+ inAppNotification?.maybeUpdateWithPortError(
+ wireguardPort = settings.wireguardPort(),
+ availablePorts = portRanges,
+ )
+ }
+ }
.map(::listOfNotNull)
.distinctUntilChanged()
@@ -31,4 +59,31 @@ class TunnelStateNotificationUseCase(private val connectionProxy: ConnectionProx
is TunnelState.Connected,
is TunnelState.Disconnected -> null
}
+
+ private fun InAppNotification.maybeUpdateWithPortError(
+ wireguardPort: Constraint<Port>,
+ availablePorts: List<PortRange>,
+ ): InAppNotification =
+ if (this is InAppNotification.TunnelStateError && error.isPossiblePortError()) {
+ wireguardPort.invalidPortOrNull(availablePorts)?.let {
+ copy(
+ error =
+ ErrorState(
+ cause = ErrorStateCause.NoRelaysMatchSelectedPort(port = it),
+ isBlocking = error.isBlocking,
+ )
+ )
+ } ?: this
+ } else this
+
+ private fun ErrorState.isPossiblePortError(): Boolean =
+ cause is ErrorStateCause.TunnelParameterError &&
+ (cause as ErrorStateCause.TunnelParameterError).error ==
+ ParameterGenerationError.NoMatchingRelay
+
+ private fun Constraint<Port>.invalidPortOrNull(availablePortRanges: List<PortRange>): Port? =
+ getOrNull()?.takeIf { !it.inAnyOf(availablePortRanges) }
+
+ private fun Settings?.wireguardPort() =
+ this?.relaySettings?.relayConstraints?.wireguardConstraints?.port ?: Constraint.Any
}
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/TunnelStateNotificationUseCaseTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/TunnelStateNotificationUseCaseTest.kt
index 8d2ece124b..6544913748 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/TunnelStateNotificationUseCaseTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/TunnelStateNotificationUseCaseTest.kt
@@ -5,17 +5,27 @@ import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.mockk
import io.mockk.unmockkAll
-import kotlin.test.assertEquals
-import kotlin.test.assertTrue
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
+import net.mullvad.mullvadvpn.lib.common.test.assertLists
import net.mullvad.mullvadvpn.lib.model.ActionAfterDisconnect
+import net.mullvad.mullvadvpn.lib.model.Constraint
import net.mullvad.mullvadvpn.lib.model.ErrorState
+import net.mullvad.mullvadvpn.lib.model.ErrorStateCause.NoRelaysMatchSelectedPort
+import net.mullvad.mullvadvpn.lib.model.ErrorStateCause.TunnelParameterError
import net.mullvad.mullvadvpn.lib.model.InAppNotification
+import net.mullvad.mullvadvpn.lib.model.ParameterGenerationError
+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.lib.model.TunnelState
import net.mullvad.mullvadvpn.lib.shared.ConnectionProxy
+import net.mullvad.mullvadvpn.repository.RelayListRepository
+import net.mullvad.mullvadvpn.repository.SettingsRepository
import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
@@ -24,18 +34,28 @@ import org.junit.jupiter.api.extension.ExtendWith
class TunnelStateNotificationUseCaseTest {
private val mockConnectionProxy: ConnectionProxy = mockk()
+ private val mockRelayListRepository: RelayListRepository = mockk()
+ private val mockSettingsRepository: SettingsRepository = mockk()
private lateinit var tunnelStateNotificationUseCase: TunnelStateNotificationUseCase
private val tunnelState = MutableStateFlow<TunnelState>(TunnelState.Disconnected())
+ private val portRanges = MutableStateFlow<List<PortRange>>(emptyList())
+ private val settingsFlow = MutableStateFlow<Settings>(mockk(relaxed = true))
@BeforeEach
fun setup() {
MockKAnnotations.init(this)
every { mockConnectionProxy.tunnelState } returns tunnelState
+ every { mockRelayListRepository.portRanges } returns portRanges
+ every { mockSettingsRepository.settingsUpdates } returns settingsFlow
tunnelStateNotificationUseCase =
- TunnelStateNotificationUseCase(connectionProxy = mockConnectionProxy)
+ TunnelStateNotificationUseCase(
+ connectionProxy = mockConnectionProxy,
+ relayListRepository = mockRelayListRepository,
+ settingsRepository = mockSettingsRepository,
+ )
}
@AfterEach
@@ -46,15 +66,16 @@ class TunnelStateNotificationUseCaseTest {
@Test
fun `initial state should be empty`() = runTest {
// Arrange, Act, Assert
- tunnelStateNotificationUseCase().test { assertTrue { awaitItem().isEmpty() } }
+ tunnelStateNotificationUseCase().test { assertTrue(awaitItem().isEmpty()) }
}
@Test
fun `when TunnelState is error use case should emit TunnelStateError notification`() = runTest {
tunnelStateNotificationUseCase().test {
// Arrange, Act
- assertEquals(emptyList(), awaitItem())
+ assertLists(emptyList(), awaitItem())
val errorState: ErrorState = mockk()
+ every { errorState.cause } returns mockk()
tunnelState.emit(TunnelState.Error(errorState))
// Assert
@@ -67,11 +88,66 @@ class TunnelStateNotificationUseCaseTest {
runTest {
tunnelStateNotificationUseCase().test {
// Arrange, Act
- assertEquals(emptyList(), awaitItem())
+ assertLists(emptyList(), awaitItem())
tunnelState.emit(TunnelState.Disconnecting(ActionAfterDisconnect.Block))
// Assert
assertEquals(listOf(InAppNotification.TunnelStateBlocked), awaitItem())
}
}
+
+ @Test
+ fun `when error cause is TunnelParameterError and port is not in range use case should emit NoRelaysMatchSelectedPort error`() =
+ runTest {
+ tunnelStateNotificationUseCase().test {
+ // Arrange, Act
+ assertLists(emptyList(), awaitItem())
+ val errorState: ErrorState = mockk()
+ every { errorState.isBlocking } returns true
+ every { errorState.cause } returns
+ TunnelParameterError(ParameterGenerationError.NoMatchingRelay)
+ val settings: Settings = mockk()
+ every { settings.relaySettings.relayConstraints.wireguardConstraints.port } returns
+ Constraint.Only(Port(1))
+ val portRange = PortRange(2..3)
+ settingsFlow.emit(settings)
+ portRanges.emit(listOf(portRange))
+ tunnelState.emit(TunnelState.Error(errorState))
+
+ // Assert
+ val item = awaitItem()
+ assertTrue {
+ (item.first() as InAppNotification.TunnelStateError).error.cause is
+ NoRelaysMatchSelectedPort
+ }
+ }
+ }
+
+ @Test
+ fun `when error cause is TunnelParameterError and port is in range use case should emit TunnelParameterError error`() =
+ runTest {
+ tunnelStateNotificationUseCase().test {
+ // Arrange, Act
+ assertLists(emptyList(), awaitItem())
+ val errorState: ErrorState = mockk()
+ every { errorState.isBlocking } returns true
+ every { errorState.cause } returns
+ TunnelParameterError(ParameterGenerationError.NoMatchingRelay)
+ val settings: Settings = mockk()
+ every { settings.relaySettings.relayConstraints.wireguardConstraints.port } returns
+ Constraint.Only(Port(2))
+ val portRange = PortRange(2..3)
+ settingsFlow.emit(settings)
+ portRanges.emit(listOf(portRange))
+ tunnelState.emit(TunnelState.Error(errorState))
+
+ // Assert
+ val item = awaitItem()
+ assertEquals(listOf(InAppNotification.TunnelStateError(errorState)), item)
+ assertTrue {
+ (item.first() as InAppNotification.TunnelStateError).error.cause is
+ TunnelParameterError
+ }
+ }
+ }
}