summaryrefslogtreecommitdiffhomepage
path: root/android/app/src
diff options
context:
space:
mode:
authorDavid Göransson <david.goransson@mullvad.net>2024-11-18 14:23:05 +0100
committerDavid Göransson <david.goransson@mullvad.net>2024-11-27 09:00:18 +0100
commit1bb7fc7ebaa2837ed9f9d28c2bb5a6fd91033988 (patch)
treea83565926fb753a2dd9fd24f0e2bd07262e4507e /android/app/src
parent0d155385e1cb7075012bd270de0398d83a438bc5 (diff)
downloadmullvadvpn-1bb7fc7ebaa2837ed9f9d28c2bb5a6fd91033988.tar.xz
mullvadvpn-1bb7fc7ebaa2837ed9f9d28c2bb5a6fd91033988.zip
Handle legacy always-on vpn profiles
Co-authored-by: Jonatan Rhodin <jonatan.rhodin@mullvad.net>
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.kt3
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationData.kt140
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt55
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/MullvadApp.kt28
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/util/CreateVpnProfile.kt14
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/util/RequestVpnPermission.kt28
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/AppModule.kt6
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt5
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/receiver/BootCompletedReceiver.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt22
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnProfileViewModel.kt (renamed from android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnPermissionViewModel.kt)14
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModelTest.kt5
13 files changed, 198 insertions, 128 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 487e739025..9087fe2f6c 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
@@ -241,7 +241,7 @@ class ConnectScreenTest {
}
// Assert
- onNodeWithText("FAILED TO SECURE CONNECTION").assertExists()
+ onNodeWithText("FAILED TO CONNECT").assertExists()
onNodeWithText(mockLocationName).assertExists()
onNodeWithText("Dismiss").assertExists()
onNodeWithText(text = "Critical error (your attention is required)", ignoreCase = true)
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 538a746b99..bc1aaeb641 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
@@ -21,6 +21,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.text.toUpperCase
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout
@@ -129,7 +130,7 @@ private fun Notification(notificationBannerData: NotificationData) {
},
)
Text(
- text = title.uppercase(),
+ text = title.toUpperCase(),
modifier =
Modifier.constrainAs(textTitle) {
top.linkTo(parent.top)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationData.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationData.kt
index 5318933852..97a7b9a483 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationData.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationData.kt
@@ -3,7 +3,6 @@ package net.mullvad.mullvadvpn.compose.component.notificationbanner
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.OpenInNew
import androidx.compose.material.icons.filled.Clear
-import androidx.compose.material.icons.filled.OpenInNew
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
@@ -13,26 +12,29 @@ import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.core.text.HtmlCompat
+import java.net.InetAddress
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.compose.extensions.getExpiryQuantityString
import net.mullvad.mullvadvpn.compose.extensions.toAnnotatedString
-import net.mullvad.mullvadvpn.lib.common.util.getErrorNotificationResources
+import net.mullvad.mullvadvpn.lib.model.AuthFailedError
import net.mullvad.mullvadvpn.lib.model.ErrorState
+import net.mullvad.mullvadvpn.lib.model.ErrorStateCause
+import net.mullvad.mullvadvpn.lib.model.ParameterGenerationError
import net.mullvad.mullvadvpn.repository.InAppNotification
import net.mullvad.mullvadvpn.ui.notification.StatusLevel
data class NotificationData(
- val title: String,
+ val title: AnnotatedString,
val message: AnnotatedString? = null,
val statusLevel: StatusLevel,
val action: NotificationAction? = null,
) {
constructor(
title: String,
- message: String?,
+ message: String? = null,
statusLevel: StatusLevel,
- action: NotificationAction?,
- ) : this(title, message?.let { AnnotatedString(it) }, statusLevel, action)
+ action: NotificationAction? = null,
+ ) : this(AnnotatedString(title), message?.let { AnnotatedString(it) }, statusLevel, action)
}
data class NotificationAction(
@@ -51,22 +53,11 @@ fun InAppNotification.toNotificationData(
when (this) {
is InAppNotification.NewDevice ->
NotificationData(
- title = stringResource(id = R.string.new_device_notification_title),
+ title =
+ AnnotatedString(stringResource(id = R.string.new_device_notification_title)),
message =
- HtmlCompat.fromHtml(
- stringResource(
- id = R.string.new_device_notification_message,
- deviceName,
- ),
- HtmlCompat.FROM_HTML_MODE_COMPACT,
- )
- .toAnnotatedString(
- boldSpanStyle =
- SpanStyle(
- color = MaterialTheme.colorScheme.onSurface,
- fontWeight = FontWeight.ExtraBold,
- )
- ),
+ stringResource(id = R.string.new_device_notification_message, deviceName)
+ .formatWithHtml(),
statusLevel = StatusLevel.Info,
action =
NotificationAction(
@@ -111,23 +102,94 @@ fun InAppNotification.toNotificationData(
@Composable
private fun errorMessageBannerData(error: ErrorState) =
- error.getErrorNotificationResources(LocalContext.current).run {
- NotificationData(
- title = stringResource(id = titleResourceId),
- message =
- HtmlCompat.fromHtml(
- optionalMessageArgument?.let { stringResource(id = messageResourceId, it) }
- ?: stringResource(id = messageResourceId),
- HtmlCompat.FROM_HTML_MODE_COMPACT,
- )
- .toAnnotatedString(
- boldSpanStyle =
- SpanStyle(
- color = MaterialTheme.colorScheme.onSurface,
- fontWeight = FontWeight.ExtraBold,
- )
- ),
- statusLevel = StatusLevel.Error,
- action = null,
+ NotificationData(
+ title = error.title().formatWithHtml(),
+ message = error.message().formatWithHtml(),
+ statusLevel = StatusLevel.Error,
+ action = null,
+ )
+
+@Composable
+private fun String.formatWithHtml(): AnnotatedString =
+ HtmlCompat.fromHtml(this, HtmlCompat.FROM_HTML_MODE_COMPACT)
+ .toAnnotatedString(
+ boldSpanStyle =
+ SpanStyle(
+ color = MaterialTheme.colorScheme.onSurface,
+ fontWeight = FontWeight.ExtraBold,
+ )
)
+
+@Composable
+private fun ErrorState.title(): String {
+ val cause = this.cause
+ return when {
+ cause is ErrorStateCause.InvalidDnsServers -> stringResource(R.string.blocking_internet)
+ cause is ErrorStateCause.NotPrepared ->
+ stringResource(R.string.vpn_permission_error_notification_title)
+ cause is ErrorStateCause.OtherAlwaysOnApp ->
+ stringResource(R.string.always_on_vpn_error_notification_title, cause.appName)
+ cause is ErrorStateCause.OtherLegacyAlwaysOnApp ->
+ stringResource(R.string.legacy_always_on_vpn_error_notification_title)
+ isBlocking -> stringResource(R.string.blocking_internet)
+ else -> stringResource(R.string.critical_error)
}
+}
+
+@Composable
+private fun ErrorState.message(): String {
+ val cause = this.cause
+ return when {
+ isBlocking -> cause.errorMessageId()
+ else -> stringResource(R.string.failed_to_block_internet)
+ }
+}
+
+@Composable
+private fun ErrorStateCause.errorMessageId(): String =
+ when (this) {
+ is ErrorStateCause.AuthFailed -> stringResource(error.errorMessageId())
+ is ErrorStateCause.Ipv6Unavailable -> stringResource(R.string.ipv6_unavailable)
+ is ErrorStateCause.FirewallPolicyError -> stringResource(R.string.set_firewall_policy_error)
+ is ErrorStateCause.DnsError -> stringResource(R.string.set_dns_error)
+ is ErrorStateCause.StartTunnelError -> stringResource(R.string.start_tunnel_error)
+ is ErrorStateCause.IsOffline -> stringResource(R.string.is_offline)
+ is ErrorStateCause.TunnelParameterError -> stringResource(error.errorMessageId())
+ is ErrorStateCause.NotPrepared ->
+ stringResource(R.string.vpn_permission_error_notification_message)
+ is ErrorStateCause.OtherAlwaysOnApp ->
+ stringResource(R.string.always_on_vpn_error_notification_content, appName)
+ is ErrorStateCause.OtherLegacyAlwaysOnApp ->
+ stringResource(R.string.legacy_always_on_vpn_error_notification_content)
+ is ErrorStateCause.InvalidDnsServers ->
+ stringResource(
+ R.string.invalid_dns_servers,
+ addresses.joinToString { address -> address.addressString() },
+ )
+ }
+
+private fun AuthFailedError.errorMessageId(): Int =
+ when (this) {
+ AuthFailedError.ExpiredAccount -> R.string.account_credit_has_expired
+ AuthFailedError.InvalidAccount,
+ AuthFailedError.TooManyConnections,
+ AuthFailedError.Unknown -> R.string.auth_failed
+ }
+
+private fun ParameterGenerationError.errorMessageId(): Int =
+ when (this) {
+ ParameterGenerationError.NoMatchingRelay,
+ ParameterGenerationError.NoMatchingBridgeRelay -> {
+ R.string.no_matching_relay
+ }
+ ParameterGenerationError.NoWireguardKey -> R.string.no_wireguard_key
+ ParameterGenerationError.CustomTunnelHostResultionError ->
+ R.string.custom_tunnel_host_resolution_error
+ }
+
+private fun InetAddress.addressString(): String {
+ val hostNameAndAddress = this.toString().split('/', limit = 2)
+ val address = hostNameAndAddress[1]
+
+ return address
+}
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 c3640979d3..b143e72f25 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
@@ -61,7 +61,6 @@ import com.ramcosta.composedestinations.generated.destinations.SettingsDestinati
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.result.ResultRecipient
import kotlinx.coroutines.launch
-import mullvad_daemon.management_interface.tunnelState
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.compose.button.ConnectionButton
import net.mullvad.mullvadvpn.compose.button.SwitchLocationButton
@@ -84,8 +83,8 @@ import net.mullvad.mullvadvpn.compose.test.RECONNECT_BUTTON_TEST_TAG
import net.mullvad.mullvadvpn.compose.test.SELECT_LOCATION_BUTTON_TEST_TAG
import net.mullvad.mullvadvpn.compose.transitions.HomeTransition
import net.mullvad.mullvadvpn.compose.util.CollectSideEffectWithLifecycle
+import net.mullvad.mullvadvpn.compose.util.CreateVpnProfile
import net.mullvad.mullvadvpn.compose.util.OnNavResultValue
-import net.mullvad.mullvadvpn.compose.util.RequestVpnPermission
import net.mullvad.mullvadvpn.compose.util.showSnackbarImmediately
import net.mullvad.mullvadvpn.constant.SECURE_ZOOM
import net.mullvad.mullvadvpn.constant.SECURE_ZOOM_ANIMATION_MILLIS
@@ -100,6 +99,7 @@ import net.mullvad.mullvadvpn.lib.model.GeoIpLocation
import net.mullvad.mullvadvpn.lib.model.LatLong
import net.mullvad.mullvadvpn.lib.model.Latitude
import net.mullvad.mullvadvpn.lib.model.Longitude
+import net.mullvad.mullvadvpn.lib.model.PrepareError
import net.mullvad.mullvadvpn.lib.model.TunnelState
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
@@ -142,9 +142,9 @@ fun Connect(
val snackbarHostState = remember { SnackbarHostState() }
- val launchVpnPermission =
- rememberLauncherForActivityResult(RequestVpnPermission()) {
- connectViewModel.requestVpnPermissionResult(it)
+ val createVpnProfile =
+ rememberLauncherForActivityResult(CreateVpnProfile()) {
+ connectViewModel.createVpnProfileResult(it)
}
val openAccountPage = LocalUriHandler.current.createOpenAccountPageHook()
@@ -154,9 +154,8 @@ fun Connect(
minActiveState = Lifecycle.State.RESUMED,
) { sideEffect ->
when (sideEffect) {
- is ConnectViewModel.UiSideEffect.OpenAccountManagementPageInBrowser -> {
+ is ConnectViewModel.UiSideEffect.OpenAccountManagementPageInBrowser ->
openAccountPage(sideEffect.token)
- }
is ConnectViewModel.UiSideEffect.OutOfTime ->
navigator.navigate(OutOfTimeDestination) {
@@ -170,7 +169,24 @@ fun Connect(
popUpTo(NavGraphs.root) { inclusive = true }
}
- is ConnectViewModel.UiSideEffect.NoVpnPermission -> launchVpnPermission.launch(Unit)
+ is ConnectViewModel.UiSideEffect.NotPrepared ->
+ when (sideEffect.prepareError) {
+ is PrepareError.OtherLegacyAlwaysOnVpn ->
+ launch {
+ snackbarHostState.showSnackbarImmediately(
+ message = sideEffect.prepareError.toMessage(context)
+ )
+ }
+
+ is PrepareError.OtherAlwaysOnApp ->
+ launch {
+ snackbarHostState.showSnackbarImmediately(
+ message = sideEffect.prepareError.toMessage(context)
+ )
+ }
+ is PrepareError.NotPrepared ->
+ createVpnProfile.launch(sideEffect.prepareError.prepareIntent)
+ }
is ConnectViewModel.UiSideEffect.ConnectError ->
launch {
snackbarHostState.showSnackbarImmediately(
@@ -178,13 +194,12 @@ fun Connect(
)
}
- is ConnectViewModel.UiSideEffect.OpenUri -> {
+ is ConnectViewModel.UiSideEffect.OpenUri ->
try {
uriHandler.openUri(sideEffect.uri.toString())
} catch (e: IllegalArgumentException) {
Logger.w("Failed to open uri", e)
}
- }
}
}
@@ -581,15 +596,17 @@ fun GeoIpLocation.toLatLong() =
private fun ConnectViewModel.UiSideEffect.ConnectError.toMessage(context: Context): String =
when (this) {
- ConnectViewModel.UiSideEffect.ConnectError.NoVpnPermission ->
- context.getString(R.string.vpn_permission_denied_error)
-
- is ConnectViewModel.UiSideEffect.ConnectError.AlwaysOnVpn ->
- // Snackbar currently do not support annotated string
- context
- .getString(R.string.always_on_vpn_error_notification_content, appName)
- .removeHtmlTags()
-
ConnectViewModel.UiSideEffect.ConnectError.Generic ->
context.getString(R.string.error_occurred)
+
+ ConnectViewModel.UiSideEffect.ConnectError.PermissionDenied ->
+ context.getString(R.string.vpn_permission_denied_error)
}
+
+private fun PrepareError.OtherLegacyAlwaysOnVpn.toMessage(context: Context) =
+ context
+ .getString(R.string.always_on_vpn_error_notification_content, "Legacy app")
+ .removeHtmlTags()
+
+private fun PrepareError.OtherAlwaysOnApp.toMessage(context: Context) =
+ context.getString(R.string.always_on_vpn_error_notification_content, appName).removeHtmlTags()
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/MullvadApp.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/MullvadApp.kt
index 332992c4e5..f52a4e8879 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/MullvadApp.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/MullvadApp.kt
@@ -7,9 +7,11 @@ import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.navigation.NavHostController
+import arrow.core.merge
import co.touchlab.kermit.Logger
import com.ramcosta.composedestinations.DestinationsNavHost
import com.ramcosta.composedestinations.generated.NavGraphs
@@ -17,11 +19,14 @@ import com.ramcosta.composedestinations.generated.destinations.NoDaemonDestinati
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.rememberNavHostEngine
import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator
-import net.mullvad.mullvadvpn.compose.util.RequestVpnPermission
+import net.mullvad.mullvadvpn.compose.util.CreateVpnProfile
+import net.mullvad.mullvadvpn.lib.common.util.prepareVpnSafe
+import net.mullvad.mullvadvpn.lib.model.PrepareError
+import net.mullvad.mullvadvpn.lib.model.Prepared
import net.mullvad.mullvadvpn.viewmodel.DaemonScreenEvent
import net.mullvad.mullvadvpn.viewmodel.NoDaemonViewModel
-import net.mullvad.mullvadvpn.viewmodel.VpnPermissionSideEffect
-import net.mullvad.mullvadvpn.viewmodel.VpnPermissionViewModel
+import net.mullvad.mullvadvpn.viewmodel.VpnProfileSideEffect
+import net.mullvad.mullvadvpn.viewmodel.VpnProfileViewModel
import org.koin.androidx.compose.koinViewModel
@OptIn(ExperimentalComposeUiApi::class)
@@ -32,7 +37,7 @@ fun MullvadApp() {
val navigator: DestinationsNavigator = navHostController.rememberDestinationsNavigator()
val serviceVm = koinViewModel<NoDaemonViewModel>()
- val permissionVm = koinViewModel<VpnPermissionViewModel>()
+ val permissionVm = koinViewModel<VpnProfileViewModel>()
DisposableEffect(Unit) {
navHostController.addOnDestinationChangedListener(serviceVm)
@@ -64,11 +69,20 @@ fun MullvadApp() {
// Ask for VPN Permission
val launchVpnPermission =
- rememberLauncherForActivityResult(RequestVpnPermission()) { _ -> permissionVm.connect() }
+ rememberLauncherForActivityResult(CreateVpnProfile()) { _ -> permissionVm.connect() }
+ val context = LocalContext.current
LaunchedEffect(Unit) {
permissionVm.uiSideEffect.collect {
- if (it is VpnPermissionSideEffect.ShowDialog) {
- launchVpnPermission.launch(Unit)
+ if (it is VpnProfileSideEffect.RequestVpnProfile) {
+ val prepareResult = context.prepareVpnSafe().merge()
+ when (prepareResult) {
+ is PrepareError.NotPrepared ->
+ launchVpnPermission.launch(prepareResult.prepareIntent)
+ // If legacy or other always on connect at let daemon generate a error state
+ is PrepareError.OtherLegacyAlwaysOnVpn,
+ is PrepareError.OtherAlwaysOnApp,
+ Prepared -> permissionVm.connect()
+ }
}
}
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/util/CreateVpnProfile.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/util/CreateVpnProfile.kt
new file mode 100644
index 0000000000..750ca6485c
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/util/CreateVpnProfile.kt
@@ -0,0 +1,14 @@
+package net.mullvad.mullvadvpn.compose.util
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import androidx.activity.result.contract.ActivityResultContract
+
+class CreateVpnProfile : ActivityResultContract<Intent, Boolean>() {
+ override fun createIntent(context: Context, input: Intent): Intent = input
+
+ override fun parseResult(resultCode: Int, intent: Intent?): Boolean {
+ return resultCode == Activity.RESULT_OK
+ }
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/util/RequestVpnPermission.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/util/RequestVpnPermission.kt
deleted file mode 100644
index f198a3159c..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/util/RequestVpnPermission.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-package net.mullvad.mullvadvpn.compose.util
-
-import android.app.Activity
-import android.content.Context
-import android.content.Intent
-import android.net.VpnService
-import androidx.activity.result.contract.ActivityResultContract
-
-class RequestVpnPermission : ActivityResultContract<Unit, Boolean>() {
- override fun createIntent(context: Context, input: Unit): Intent {
- return VpnService.prepare(context)!!
- }
-
- override fun parseResult(resultCode: Int, intent: Intent?): Boolean {
- return resultCode == Activity.RESULT_OK
- }
-
- // We expect this permission to only be requested when the permission is missing. However,
- // if it for some reason is called incorrectly we will skip the call to create intent
- // to avoid crashing. The app will then proceed as the user accepted the permission.
- override fun getSynchronousResult(context: Context, input: Unit): SynchronousResult<Boolean>? {
- return if (VpnService.prepare(context) == null) {
- SynchronousResult(true)
- } else {
- null
- }
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/AppModule.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/AppModule.kt
index e8cd424156..3128870ae5 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/AppModule.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/AppModule.kt
@@ -13,7 +13,7 @@ import net.mullvad.mullvadvpn.lib.shared.ConnectionProxy
import net.mullvad.mullvadvpn.lib.shared.DeviceRepository
import net.mullvad.mullvadvpn.lib.shared.LocaleRepository
import net.mullvad.mullvadvpn.lib.shared.RelayLocationTranslationRepository
-import net.mullvad.mullvadvpn.lib.shared.VpnPermissionRepository
+import net.mullvad.mullvadvpn.lib.shared.VpnProfileUseCase
import org.koin.android.ext.koin.androidContext
import org.koin.core.qualifier.named
import org.koin.dsl.module
@@ -29,11 +29,13 @@ val appModule = module {
scope = MainScope(),
)
}
+
+ single { VpnProfileUseCase(androidContext()) }
+
single { BuildVersion(BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE) }
single { IntentProvider() }
single { AccountRepository(get(), get(), MainScope()) }
single { DeviceRepository(get()) }
- single { VpnPermissionRepository(androidContext()) }
single { ConnectionProxy(get(), get(), get()) }
single { LocaleRepository(get()) }
single { RelayLocationTranslationRepository(get(), get(), MainScope()) }
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 1d62de5bb2..f43f1caf8f 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
@@ -91,7 +91,7 @@ import net.mullvad.mullvadvpn.viewmodel.SplitTunnelingViewModel
import net.mullvad.mullvadvpn.viewmodel.Udp2TcpSettingsViewModel
import net.mullvad.mullvadvpn.viewmodel.ViewLogsViewModel
import net.mullvad.mullvadvpn.viewmodel.VoucherDialogViewModel
-import net.mullvad.mullvadvpn.viewmodel.VpnPermissionViewModel
+import net.mullvad.mullvadvpn.viewmodel.VpnProfileViewModel
import net.mullvad.mullvadvpn.viewmodel.VpnSettingsViewModel
import net.mullvad.mullvadvpn.viewmodel.WelcomeViewModel
import net.mullvad.mullvadvpn.viewmodel.WireguardCustomPortDialogViewModel
@@ -206,7 +206,6 @@ val uiModule = module {
get(),
get(),
get(),
- get(),
IS_PLAY_BUILD,
get(named(SELF_PACKAGE_NAME)),
)
@@ -237,7 +236,7 @@ val uiModule = module {
viewModel { DeleteCustomListConfirmationViewModel(get(), get()) }
viewModel { ServerIpOverridesViewModel(get(), get()) }
viewModel { ResetServerIpOverridesConfirmationViewModel(get()) }
- viewModel { VpnPermissionViewModel(get(), get()) }
+ viewModel { VpnProfileViewModel(get(), get()) }
viewModel { ApiAccessListViewModel(get()) }
viewModel { EditApiAccessMethodViewModel(get(), get(), get()) }
viewModel { SaveApiAccessMethodViewModel(get(), get()) }
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/receiver/BootCompletedReceiver.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/receiver/BootCompletedReceiver.kt
index 9f153e724b..3ab3750c5e 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/receiver/BootCompletedReceiver.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/receiver/BootCompletedReceiver.kt
@@ -3,10 +3,10 @@ package net.mullvad.mullvadvpn.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
-import android.net.VpnService
import co.touchlab.kermit.Logger
import net.mullvad.mullvadvpn.lib.common.constant.KEY_CONNECT_ACTION
import net.mullvad.mullvadvpn.lib.common.constant.VPN_SERVICE_CLASS
+import net.mullvad.mullvadvpn.lib.common.util.prepareVpnSafe
class BootCompletedReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
@@ -16,7 +16,7 @@ class BootCompletedReceiver : BroadcastReceiver() {
}
private fun startAndConnectTunnel(context: Context) {
- val hasVpnPermission = VpnService.prepare(context) == null
+ val hasVpnPermission = context.prepareVpnSafe().isRight()
Logger.i("AutoStart on boot and connect, hasVpnPermission: $hasVpnPermission")
if (hasVpnPermission) {
val intent =
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 5572f93961..42838d75d6 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
@@ -21,12 +21,12 @@ import net.mullvad.mullvadvpn.compose.state.ConnectUiState
import net.mullvad.mullvadvpn.lib.model.ActionAfterDisconnect
import net.mullvad.mullvadvpn.lib.model.ConnectError
import net.mullvad.mullvadvpn.lib.model.DeviceState
+import net.mullvad.mullvadvpn.lib.model.PrepareError
import net.mullvad.mullvadvpn.lib.model.TunnelState
import net.mullvad.mullvadvpn.lib.model.WebsiteAuthToken
import net.mullvad.mullvadvpn.lib.shared.AccountRepository
import net.mullvad.mullvadvpn.lib.shared.ConnectionProxy
import net.mullvad.mullvadvpn.lib.shared.DeviceRepository
-import net.mullvad.mullvadvpn.lib.shared.VpnPermissionRepository
import net.mullvad.mullvadvpn.repository.InAppNotificationController
import net.mullvad.mullvadvpn.repository.NewDeviceRepository
import net.mullvad.mullvadvpn.usecase.LastKnownLocationUseCase
@@ -48,7 +48,6 @@ class ConnectViewModel(
private val paymentUseCase: PaymentUseCase,
private val connectionProxy: ConnectionProxy,
lastKnownLocationUseCase: LastKnownLocationUseCase,
- private val vpnPermissionRepository: VpnPermissionRepository,
private val resources: Resources,
private val isPlayBuild: Boolean,
private val packageName: String,
@@ -138,23 +137,20 @@ class ConnectViewModel(
viewModelScope.launch {
connectionProxy.connect().onLeft { connectError ->
when (connectError) {
- ConnectError.NoVpnPermission -> _uiSideEffect.send(UiSideEffect.NoVpnPermission)
- is ConnectError.Unknown -> {
- _uiSideEffect.send(UiSideEffect.ConnectError.Generic)
- }
+ is ConnectError.Unknown -> _uiSideEffect.send(UiSideEffect.ConnectError.Generic)
+ is ConnectError.NotPrepared ->
+ _uiSideEffect.send(UiSideEffect.NotPrepared(connectError.error))
}
}
}
}
- fun requestVpnPermissionResult(hasVpnPermission: Boolean) {
+ fun createVpnProfileResult(hasVpnPermission: Boolean) {
viewModelScope.launch {
if (hasVpnPermission) {
connectionProxy.connect()
} else {
- vpnPermissionRepository.getAlwaysOnVpnAppName()?.let {
- _uiSideEffect.send(UiSideEffect.ConnectError.AlwaysOnVpn(it))
- } ?: _uiSideEffect.send(UiSideEffect.ConnectError.NoVpnPermission)
+ _uiSideEffect.send(UiSideEffect.ConnectError.PermissionDenied)
}
}
}
@@ -206,14 +202,12 @@ class ConnectViewModel(
data object RevokedDevice : UiSideEffect
- data object NoVpnPermission : UiSideEffect
+ data class NotPrepared(val prepareError: PrepareError) : UiSideEffect
sealed interface ConnectError : UiSideEffect {
data object Generic : ConnectError
- data object NoVpnPermission : ConnectError
-
- data class AlwaysOnVpn(val appName: String) : ConnectError
+ data object PermissionDenied : ConnectError
}
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnPermissionViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnProfileViewModel.kt
index 1e5972b538..cb1a2862bf 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnPermissionViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnProfileViewModel.kt
@@ -9,19 +9,19 @@ import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch
-import net.mullvad.mullvadvpn.lib.common.constant.KEY_REQUEST_VPN_PERMISSION
+import net.mullvad.mullvadvpn.lib.common.constant.KEY_REQUEST_VPN_PROFILE
import net.mullvad.mullvadvpn.lib.intent.IntentProvider
import net.mullvad.mullvadvpn.lib.shared.ConnectionProxy
-class VpnPermissionViewModel(
+class VpnProfileViewModel(
intentProvider: IntentProvider,
private val connectionProxy: ConnectionProxy,
) : ViewModel() {
- val uiSideEffect: Flow<VpnPermissionSideEffect> =
+ val uiSideEffect: Flow<VpnProfileSideEffect> =
intentProvider.intents
- .filter { it?.action == KEY_REQUEST_VPN_PERMISSION }
+ .filter { it?.action == KEY_REQUEST_VPN_PROFILE }
.distinctUntilChanged()
- .map { VpnPermissionSideEffect.ShowDialog }
+ .map { VpnProfileSideEffect.RequestVpnProfile }
.shareIn(viewModelScope, SharingStarted.WhileSubscribed())
fun connect() {
@@ -29,6 +29,6 @@ class VpnPermissionViewModel(
}
}
-sealed interface VpnPermissionSideEffect {
- data object ShowDialog : VpnPermissionSideEffect
+sealed interface VpnProfileSideEffect {
+ data object RequestVpnProfile : VpnProfileSideEffect
}
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 3dada2a433..0d61b3e300 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
@@ -30,7 +30,6 @@ import net.mullvad.mullvadvpn.lib.model.WebsiteAuthToken
import net.mullvad.mullvadvpn.lib.shared.AccountRepository
import net.mullvad.mullvadvpn.lib.shared.ConnectionProxy
import net.mullvad.mullvadvpn.lib.shared.DeviceRepository
-import net.mullvad.mullvadvpn.lib.shared.VpnPermissionRepository
import net.mullvad.mullvadvpn.repository.InAppNotification
import net.mullvad.mullvadvpn.repository.InAppNotificationController
import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
@@ -87,9 +86,6 @@ class ConnectViewModelTest {
// Last known location
private val mockLastKnownLocationUseCase: LastKnownLocationUseCase = mockk()
- // VpnPermissionRepository
- private val mockVpnPermissionRepository: VpnPermissionRepository = mockk(relaxed = true)
-
@BeforeEach
fun setup() {
every { mockServiceConnectionManager.connectionState } returns serviceConnectionState
@@ -124,7 +120,6 @@ class ConnectViewModelTest {
selectedLocationTitleUseCase = mockSelectedLocationTitleUseCase,
connectionProxy = mockConnectionProxy,
lastKnownLocationUseCase = mockLastKnownLocationUseCase,
- vpnPermissionRepository = mockVpnPermissionRepository,
resources = mockk(),
isPlayBuild = false,
packageName = "net.mullvad.mullvadvpn",