summaryrefslogtreecommitdiffhomepage
path: root/android/app/src
diff options
context:
space:
mode:
authorJonatan Rhodin <jonatan.rhodin@mullvad.net>2025-05-05 23:29:22 +0200
committerJonatan Rhodin <jonatan.rhodin@mullvad.net>2025-05-06 12:39:13 +0200
commit2ea5bf3bff4a68b04ee805419b952917cd15ce59 (patch)
treebea1dd4405b5d4345f23f9bbf9b504d72491378b /android/app/src
parentc228ff918ac023c1c76bb4cf4f3d595a181cea63 (diff)
downloadmullvadvpn-2ea5bf3bff4a68b04ee805419b952917cd15ce59.tar.xz
mullvadvpn-2ea5bf3bff4a68b04ee805419b952917cd15ce59.zip
Fix go to vpn settings on TV devices
- Do not show the action on devices without vpn settings - Handle exceptions when starting vpn settings activity
Diffstat (limited to 'android/app/src')
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt31
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt1
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt10
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModelTest.kt35
4 files changed, 70 insertions, 7 deletions
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 2a3474c755..45e9bdee52 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
@@ -231,7 +231,7 @@ fun Connect(
createVpnProfile.launch(sideEffect.prepareError.prepareIntent)
}
- is ConnectViewModel.UiSideEffect.ConnectError.Generic ->
+ ConnectViewModel.UiSideEffect.ConnectError.Generic ->
snackbarHostState.showSnackbarImmediately(
message = context.getString(R.string.error_occurred)
)
@@ -239,10 +239,31 @@ fun Connect(
is ConnectViewModel.UiSideEffect.ConnectError.PermissionDenied -> {
launch {
snackbarHostState.showSnackbarImmediately(
- message = context.getString(R.string.vpn_permission_denied_error),
- actionLabel = context.getString(R.string.go_to_vpn_settings),
- withDismissAction = true,
- onAction = context::openVpnSettings,
+ message =
+ context.getString(
+ if (sideEffect.systemVpnSettingsAvailable) {
+ R.string.vpn_permission_denied_error
+ } else {
+ R.string.vpn_permission_denied_error_no_vpn_settings
+ }
+ ),
+ actionLabel =
+ if (sideEffect.systemVpnSettingsAvailable) {
+ context.getString(R.string.go_to_vpn_settings)
+ } else {
+ null
+ },
+ withDismissAction = sideEffect.systemVpnSettingsAvailable,
+ onAction = {
+ context.openVpnSettings().onLeft {
+ launch {
+ snackbarHostState.showSnackbarImmediately(
+ message =
+ context.getString(R.string.vpn_settings_not_available)
+ )
+ }
+ }
+ },
)
}
}
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 776a36ccb7..c44aedd890 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
@@ -217,6 +217,7 @@ val uiModule = module {
paymentUseCase = get(),
connectionProxy = get(),
lastKnownLocationUseCase = get(),
+ systemVpnSettingsUseCase = get(),
resources = get(),
isPlayBuild = IS_PLAY_BUILD,
isFdroidBuild = IS_FDROID_BUILD,
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt
index 2ccb138a5f..e836acb844 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt
@@ -35,6 +35,7 @@ import net.mullvad.mullvadvpn.usecase.LastKnownLocationUseCase
import net.mullvad.mullvadvpn.usecase.OutOfTimeUseCase
import net.mullvad.mullvadvpn.usecase.PaymentUseCase
import net.mullvad.mullvadvpn.usecase.SelectedLocationTitleUseCase
+import net.mullvad.mullvadvpn.usecase.SystemVpnSettingsAvailableUseCase
import net.mullvad.mullvadvpn.util.combine
import net.mullvad.mullvadvpn.util.isSuccess
import net.mullvad.mullvadvpn.util.withPrev
@@ -51,6 +52,7 @@ class ConnectViewModel(
private val paymentUseCase: PaymentUseCase,
private val connectionProxy: ConnectionProxy,
lastKnownLocationUseCase: LastKnownLocationUseCase,
+ private val systemVpnSettingsUseCase: SystemVpnSettingsAvailableUseCase,
private val resources: Resources,
private val isPlayBuild: Boolean,
private val isFdroidBuild: Boolean,
@@ -154,7 +156,11 @@ class ConnectViewModel(
} else {
// Either the user denied the permission or another always-on-vpn is active (if
// Android 11+ and run from Android Studio)
- _uiSideEffect.send(UiSideEffect.ConnectError.PermissionDenied)
+ // If we don't have vpn system settings available we assume that there is no other
+ // always-on-vpn active.
+ _uiSideEffect.send(
+ UiSideEffect.ConnectError.PermissionDenied(systemVpnSettingsUseCase())
+ )
}
}
}
@@ -220,7 +226,7 @@ class ConnectViewModel(
sealed interface ConnectError : UiSideEffect {
data object Generic : ConnectError
- data object PermissionDenied : ConnectError
+ data class PermissionDenied(val systemVpnSettingsAvailable: Boolean) : ConnectError
}
}
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModelTest.kt
index ec4e9c0bbb..b298270fad 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModelTest.kt
@@ -39,6 +39,7 @@ import net.mullvad.mullvadvpn.usecase.LastKnownLocationUseCase
import net.mullvad.mullvadvpn.usecase.OutOfTimeUseCase
import net.mullvad.mullvadvpn.usecase.PaymentUseCase
import net.mullvad.mullvadvpn.usecase.SelectedLocationTitleUseCase
+import net.mullvad.mullvadvpn.usecase.SystemVpnSettingsAvailableUseCase
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
@@ -90,6 +91,9 @@ class ConnectViewModelTest {
// Last known location
private val mockLastKnownLocationUseCase: LastKnownLocationUseCase = mockk()
+ // System VPN Settings
+ private val mockSystemVpnSettingsUseCase: SystemVpnSettingsAvailableUseCase = mockk()
+
@BeforeEach
fun setup() {
every { mockServiceConnectionManager.connectionState } returns serviceConnectionState
@@ -125,6 +129,7 @@ class ConnectViewModelTest {
selectedLocationTitleUseCase = mockSelectedLocationTitleUseCase,
connectionProxy = mockConnectionProxy,
lastKnownLocationUseCase = mockLastKnownLocationUseCase,
+ systemVpnSettingsUseCase = mockSystemVpnSettingsUseCase,
resources = mockk(),
isPlayBuild = false,
isFdroidBuild = false,
@@ -334,4 +339,34 @@ class ConnectViewModelTest {
assertEquals(lastKnownLocation, result.location)
}
}
+
+ @Test
+ fun `given no vpn system setting available should return the correct permission denied`() =
+ runTest {
+ // Arrange
+ val expectedSideEffect =
+ ConnectViewModel.UiSideEffect.ConnectError.PermissionDenied(false)
+ every { mockSystemVpnSettingsUseCase.invoke() } returns false
+
+ // Act
+ viewModel.createVpnProfileResult(hasVpnPermission = false)
+
+ // Assert
+ viewModel.uiSideEffect.test { assertEquals(expectedSideEffect, awaitItem()) }
+ }
+
+ @Test
+ fun `given vpn system setting available should return the correct permission denied`() =
+ runTest {
+ // Arrange
+ val expectedSideEffect =
+ ConnectViewModel.UiSideEffect.ConnectError.PermissionDenied(true)
+ every { mockSystemVpnSettingsUseCase.invoke() } returns true
+
+ // Act
+ viewModel.createVpnProfileResult(hasVpnPermission = false)
+
+ // Assert
+ viewModel.uiSideEffect.test { assertEquals(expectedSideEffect, awaitItem()) }
+ }
}