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 538a746b9984..bc1aaeb641d3 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 531893385226..7b8ec1ef6af6 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.AlwaysOnApp -> + stringResource(R.string.always_on_vpn_error_notification_title, cause.appName) + cause is ErrorStateCause.LegacyLockdown -> + 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.AlwaysOnApp -> + stringResource(R.string.always_on_vpn_error_notification_content, appName) + is ErrorStateCause.LegacyLockdown -> + 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 71e7f66d0fa2..4a551d6d1ee8 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() @@ -170,7 +170,24 @@ fun Connect( popUpTo(NavGraphs.root) { inclusive = true } } - is ConnectViewModel.UiSideEffect.NoVpnPermission -> launchVpnPermission.launch(Unit) + is ConnectViewModel.UiSideEffect.NotPrepared -> + when (sideEffect.prepareError) { + is PrepareError.LegacyLockdown -> + 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( @@ -571,15 +588,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.LegacyLockdown.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 332992c4e5c1..5ce81aeedbb5 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() - val permissionVm = koinViewModel() + val permissionVm = koinViewModel() 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.LegacyLockdown, + 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 000000000000..750ca6485cea --- /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() { + 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 f198a3159c68..000000000000 --- 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() { - 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? { - 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 e8cd424156b3..3128870ae5ee 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 2605075ef8d3..f5a0c1642683 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 @@ -88,7 +88,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 @@ -231,7 +231,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 9f153e724b9a..3ab3750c5ee4 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 5572f9396170..4d9c92167175 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,13 @@ 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.lib.shared.VpnProfileUseCase import net.mullvad.mullvadvpn.repository.InAppNotificationController import net.mullvad.mullvadvpn.repository.NewDeviceRepository import net.mullvad.mullvadvpn.usecase.LastKnownLocationUseCase @@ -48,7 +49,7 @@ class ConnectViewModel( private val paymentUseCase: PaymentUseCase, private val connectionProxy: ConnectionProxy, lastKnownLocationUseCase: LastKnownLocationUseCase, - private val vpnPermissionRepository: VpnPermissionRepository, + private val vpnPermissionRepository: VpnProfileUseCase, private val resources: Resources, private val isPlayBuild: Boolean, private val packageName: String, @@ -138,23 +139,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 +204,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 similarity index 74% rename from android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnPermissionViewModel.kt rename to android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnProfileViewModel.kt index 1e5972b53897..cb1a2862bf68 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 = + val uiSideEffect: Flow = 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 3dada2a4332b..f6fd79d35825 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,7 @@ 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.lib.shared.VpnProfileUseCase import net.mullvad.mullvadvpn.repository.InAppNotification import net.mullvad.mullvadvpn.repository.InAppNotificationController import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager @@ -88,7 +88,7 @@ class ConnectViewModelTest { private val mockLastKnownLocationUseCase: LastKnownLocationUseCase = mockk() // VpnPermissionRepository - private val mockVpnPermissionRepository: VpnPermissionRepository = mockk(relaxed = true) + private val mockVpnPermissionRepository: VpnProfileUseCase = mockk(relaxed = true) @BeforeEach fun setup() { diff --git a/android/lib/common/build.gradle.kts b/android/lib/common/build.gradle.kts index 2c4fbe0233af..c8554b52c7b8 100644 --- a/android/lib/common/build.gradle.kts +++ b/android/lib/common/build.gradle.kts @@ -31,10 +31,11 @@ android { dependencies { implementation(projects.lib.model) implementation(projects.lib.resource) - implementation(projects.lib.talpid) + implementation(libs.arrow) implementation(libs.androidx.appcompat) implementation(libs.jodatime) implementation(libs.kotlin.stdlib) implementation(libs.kotlinx.coroutines.android) + implementation(libs.kermit) } diff --git a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/constant/IntentActions.kt b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/constant/IntentActions.kt index ea420f2d0a98..76f71d82e372 100644 --- a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/constant/IntentActions.kt +++ b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/constant/IntentActions.kt @@ -3,4 +3,4 @@ package net.mullvad.mullvadvpn.lib.common.constant // Actions const val KEY_CONNECT_ACTION = "$MULLVAD_PACKAGE_NAME.connect_action" const val KEY_DISCONNECT_ACTION = "$MULLVAD_PACKAGE_NAME.disconnect_action" -const val KEY_REQUEST_VPN_PERMISSION = "$MULLVAD_PACKAGE_NAME.request_vpn_permission" +const val KEY_REQUEST_VPN_PROFILE = "$MULLVAD_PACKAGE_NAME.request_vpn_profile" diff --git a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ContextExtensions.kt b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ContextExtensions.kt index 7ea74edfaa6c..992ae9404d13 100644 --- a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ContextExtensions.kt +++ b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ContextExtensions.kt @@ -4,7 +4,6 @@ import android.content.Context import android.content.Intent import android.net.Uri import android.provider.Settings -import net.mullvad.mullvadvpn.lib.common.util.SdkUtils.getInstalledPackagesList import net.mullvad.mullvadvpn.lib.model.WebsiteAuthToken private const val ALWAYS_ON_VPN_APP = "always_on_vpn_app" @@ -20,18 +19,6 @@ fun createAccountUri(accountUri: String, websiteAuthToken: WebsiteAuthToken?): U return Uri.parse(urlString) } -fun Context.getAlwaysOnVpnAppName(): String? { - return resolveAlwaysOnVpnPackageName() - ?.let { currentAlwaysOnVpn -> - packageManager.getInstalledPackagesList(0).singleOrNull { - it.packageName == currentAlwaysOnVpn && it.packageName != packageName - } - } - ?.applicationInfo - ?.loadLabel(packageManager) - ?.toString() -} - // NOTE: This function will return the current Always-on VPN package's name. In case of either // Always-on VPN being disabled or not being able to read the state, NULL will be returned. fun Context.resolveAlwaysOnVpnPackageName(): String? { diff --git a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ErrorNotificationMessage.kt b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ErrorNotificationMessage.kt deleted file mode 100644 index 4a5c902d9659..000000000000 --- a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ErrorNotificationMessage.kt +++ /dev/null @@ -1,7 +0,0 @@ -package net.mullvad.mullvadvpn.lib.common.util - -data class ErrorNotificationMessage( - val titleResourceId: Int, - val messageResourceId: Int, - val optionalMessageArgument: String? = null, -) diff --git a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ErrorStateExtension.kt b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ErrorStateExtension.kt deleted file mode 100644 index a61ec10c1711..000000000000 --- a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ErrorStateExtension.kt +++ /dev/null @@ -1,77 +0,0 @@ -package net.mullvad.mullvadvpn.lib.common.util - -import android.content.Context -import net.mullvad.mullvadvpn.lib.common.R -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.talpid.util.addressString - -fun ErrorState.getErrorNotificationResources(context: Context): ErrorNotificationMessage { - return when { - cause is ErrorStateCause.InvalidDnsServers -> { - ErrorNotificationMessage( - R.string.blocking_internet, - cause.errorMessageId(), - (cause as ErrorStateCause.InvalidDnsServers).addresses.joinToString { address -> - address.addressString() - }, - ) - } - cause is ErrorStateCause.VpnPermissionDenied -> { - resolveAlwaysOnVpnErrorNotificationMessage(context.getAlwaysOnVpnAppName()) - } - isBlocking -> ErrorNotificationMessage(R.string.blocking_internet, cause.errorMessageId()) - else -> ErrorNotificationMessage(R.string.critical_error, R.string.failed_to_block_internet) - } -} - -private fun resolveAlwaysOnVpnErrorNotificationMessage( - alwaysOnVpnAppName: String? -): ErrorNotificationMessage { - return if (alwaysOnVpnAppName != null) { - ErrorNotificationMessage( - R.string.always_on_vpn_error_notification_title, - R.string.always_on_vpn_error_notification_content, - alwaysOnVpnAppName, - ) - } else { - ErrorNotificationMessage( - R.string.vpn_permission_error_notification_title, - R.string.vpn_permission_error_notification_message, - ) - } -} - -fun ErrorStateCause.errorMessageId(): Int = - when (this) { - is ErrorStateCause.InvalidDnsServers -> R.string.invalid_dns_servers - is ErrorStateCause.AuthFailed -> error.errorMessageId() - is ErrorStateCause.Ipv6Unavailable -> R.string.ipv6_unavailable - is ErrorStateCause.FirewallPolicyError -> R.string.set_firewall_policy_error - is ErrorStateCause.DnsError -> R.string.set_dns_error - is ErrorStateCause.StartTunnelError -> R.string.start_tunnel_error - is ErrorStateCause.IsOffline -> R.string.is_offline - is ErrorStateCause.TunnelParameterError -> { - when (error) { - 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 - } - } - } - is ErrorStateCause.VpnPermissionDenied -> R.string.vpn_permission_denied_error - } - -fun AuthFailedError.errorMessageId(): Int = - when (this) { - AuthFailedError.ExpiredAccount -> R.string.account_credit_has_expired - AuthFailedError.InvalidAccount, - AuthFailedError.TooManyConnections, - AuthFailedError.Unknown -> R.string.auth_failed - } diff --git a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/VpnServiceUtils.kt b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/VpnServiceUtils.kt new file mode 100644 index 000000000000..8b22ed1a9f65 --- /dev/null +++ b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/VpnServiceUtils.kt @@ -0,0 +1,61 @@ +package net.mullvad.mullvadvpn.lib.common.util + +import android.content.Context +import android.content.Intent +import android.net.VpnService.prepare +import arrow.core.Either +import arrow.core.flatten +import arrow.core.left +import arrow.core.right +import co.touchlab.kermit.Logger +import net.mullvad.mullvadvpn.lib.common.util.SdkUtils.getInstalledPackagesList +import net.mullvad.mullvadvpn.lib.model.PrepareError +import net.mullvad.mullvadvpn.lib.model.Prepared + +/** + * Invoking VpnService.prepare() can result in 3 out comes: + * 1. IllegalStateException - There is a legacy VPN profile marked as always on + * 2. Intent + * - A: Can-prepare - Create Vpn profile + * - B: Always-on-VPN - Another Vpn Profile is marked as always on + * 3. null - The app has the VPN permission + * + * In case 1 and 2b, you don't know if you have a VPN profile or not. + */ +fun Context.prepareVpnSafe(): Either = + Either.catch { + val intent: Intent? = prepare(this) + intent + } + .mapLeft { + Logger.e("VpnService.prepare() failed: $it") + when (it) { + is IllegalStateException -> PrepareError.LegacyLockdown + else -> throw it + } + } + .map { intent -> + if (intent == null) { + Prepared.right() + } else { + val alwaysOnVpnApp = getAlwaysOnVpnAppName() + if (alwaysOnVpnApp == null) { + PrepareError.NotPrepared(intent).left() + } else { + PrepareError.OtherAlwaysOnApp(alwaysOnVpnApp).left() + } + } + } + .flatten() + +fun Context.getAlwaysOnVpnAppName(): String? { + return resolveAlwaysOnVpnPackageName() + ?.let { currentAlwaysOnVpn -> + packageManager.getInstalledPackagesList(0).singleOrNull { + it.packageName == currentAlwaysOnVpn && it.packageName != packageName + } + } + ?.applicationInfo + ?.loadLabel(packageManager) + ?.toString() +} diff --git a/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt index 236d4aa19ca9..2487853d1a6c 100644 --- a/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt +++ b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt @@ -35,6 +35,9 @@ import net.mullvad.mullvadvpn.lib.model.DnsState import net.mullvad.mullvadvpn.lib.model.Endpoint import net.mullvad.mullvadvpn.lib.model.ErrorState import net.mullvad.mullvadvpn.lib.model.ErrorStateCause +import net.mullvad.mullvadvpn.lib.model.ErrorStateCause.AlwaysOnApp +import net.mullvad.mullvadvpn.lib.model.ErrorStateCause.AuthFailed +import net.mullvad.mullvadvpn.lib.model.ErrorStateCause.TunnelParameterError import net.mullvad.mullvadvpn.lib.model.FeatureIndicator import net.mullvad.mullvadvpn.lib.model.GeoIpLocation import net.mullvad.mullvadvpn.lib.model.GeoLocationId @@ -113,8 +116,37 @@ internal fun ManagementInterface.TunnelState.toDomain(): TunnelState = TunnelState.Disconnecting( actionAfterDisconnect = disconnecting.afterDisconnect.toDomain() ) - ManagementInterface.TunnelState.StateCase.ERROR -> - TunnelState.Error(errorState = error.errorState.toDomain()) + ManagementInterface.TunnelState.StateCase.ERROR -> { + val alwaysOnAppError = + error.errorState.let { + if (it.hasAlwaysOnAppError()) { + AlwaysOnApp(it.alwaysOnAppError.appName) + } else { + null + } + } + + val invalidDnsServers = + error.errorState.let { + if (it.hasInvalidDnsServersError()) { + ErrorStateCause.InvalidDnsServers( + it.invalidDnsServersError.ipAddrsList.toList().map { + InetAddress.getByName(it) + } + ) + } else { + null + } + } + + TunnelState.Error( + errorState = + error.errorState.toDomain( + alwaysOnApp = alwaysOnAppError, + invalidDnsServers = invalidDnsServers, + ) + ) + } ManagementInterface.TunnelState.StateCase.STATE_NOT_SET -> TunnelState.Disconnected(location = disconnected.disconnectedLocation.toDomain()) } @@ -198,12 +230,15 @@ internal fun ManagementInterface.AfterDisconnect.toDomain(): ActionAfterDisconne throw IllegalArgumentException("Unrecognized action after disconnect") } -internal fun ManagementInterface.ErrorState.toDomain(): ErrorState = +internal fun ManagementInterface.ErrorState.toDomain( + alwaysOnApp: ErrorStateCause.AlwaysOnApp?, + invalidDnsServers: ErrorStateCause.InvalidDnsServers?, +): ErrorState = ErrorState( cause = when (cause!!) { ManagementInterface.ErrorState.Cause.AUTH_FAILED -> - ErrorStateCause.AuthFailed(authFailedError.toDomain()) + AuthFailed(authFailedError.toDomain()) ManagementInterface.ErrorState.Cause.IPV6_UNAVAILABLE -> ErrorStateCause.Ipv6Unavailable ManagementInterface.ErrorState.Cause.SET_FIREWALL_POLICY_ERROR -> @@ -212,16 +247,20 @@ internal fun ManagementInterface.ErrorState.toDomain(): ErrorState = ManagementInterface.ErrorState.Cause.START_TUNNEL_ERROR -> ErrorStateCause.StartTunnelError ManagementInterface.ErrorState.Cause.TUNNEL_PARAMETER_ERROR -> - ErrorStateCause.TunnelParameterError(parameterError.toDomain()) + TunnelParameterError(parameterError.toDomain()) ManagementInterface.ErrorState.Cause.IS_OFFLINE -> ErrorStateCause.IsOffline - ManagementInterface.ErrorState.Cause.VPN_PERMISSION_DENIED -> - ErrorStateCause.VpnPermissionDenied ManagementInterface.ErrorState.Cause.SPLIT_TUNNEL_ERROR -> ErrorStateCause.StartTunnelError ManagementInterface.ErrorState.Cause.UNRECOGNIZED, ManagementInterface.ErrorState.Cause.NEED_FULL_DISK_PERMISSIONS, ManagementInterface.ErrorState.Cause.CREATE_TUNNEL_DEVICE -> throw IllegalArgumentException("Unrecognized error state cause") + + ManagementInterface.ErrorState.Cause.NOT_PREPARED -> ErrorStateCause.NotPrepared + ManagementInterface.ErrorState.Cause.ALWAYS_ON_APP -> alwaysOnApp!! + ManagementInterface.ErrorState.Cause.LEGACY_LOCKDOWN -> + ErrorStateCause.LegacyLockdown + ManagementInterface.ErrorState.Cause.INVALID_DNS_SERVERS -> invalidDnsServers!! }, isBlocking = !hasBlockingError(), ) diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ConnectError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ConnectError.kt index 307a235314c4..6feeeee57949 100644 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ConnectError.kt +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ConnectError.kt @@ -3,5 +3,5 @@ package net.mullvad.mullvadvpn.lib.model sealed interface ConnectError { data class Unknown(val throwable: Throwable) : ConnectError - data object NoVpnPermission : ConnectError + data class NotPrepared(val error: PrepareError) : ConnectError } diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ErrorStateCause.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ErrorStateCause.kt index ef5947c89a45..cc8839c16fc0 100644 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ErrorStateCause.kt +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ErrorStateCause.kt @@ -17,7 +17,6 @@ sealed class ErrorStateCause { data object DnsError : ErrorStateCause() - // Regression data class InvalidDnsServers(val addresses: List) : ErrorStateCause() data object StartTunnelError : ErrorStateCause() @@ -26,7 +25,11 @@ sealed class ErrorStateCause { data object IsOffline : ErrorStateCause() - data object VpnPermissionDenied : ErrorStateCause() + data object NotPrepared : ErrorStateCause() + + data class AlwaysOnApp(val appName: String) : ErrorStateCause() + + data object LegacyLockdown : ErrorStateCause() } sealed interface AuthFailedError { diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationAction.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationAction.kt index ec938a9fbf67..141fe739f5a1 100644 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationAction.kt +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationAction.kt @@ -15,6 +15,6 @@ sealed interface NotificationAction { data object Dismiss : Tunnel - data object RequestPermission : Tunnel + data object RequestVpnProfile : Tunnel } } diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationTunnelState.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationTunnelState.kt index fffe86c247b8..3ca573a839ca 100644 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationTunnelState.kt +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationTunnelState.kt @@ -1,7 +1,7 @@ package net.mullvad.mullvadvpn.lib.model sealed interface NotificationTunnelState { - data class Disconnected(val hasVpnPermission: Boolean) : NotificationTunnelState + data class Disconnected(val prepareError: PrepareError?) : NotificationTunnelState data object Connecting : NotificationTunnelState @@ -18,7 +18,9 @@ sealed interface NotificationTunnelState { data object VpnPermissionDenied : Error - data object AlwaysOnVpn : Error + data class AlwaysOnVpn(val appName: String) : Error + + data object LegacyLockdown : Error data object Critical : Error } diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/PrepareError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/PrepareError.kt new file mode 100644 index 000000000000..ab492a069810 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/PrepareError.kt @@ -0,0 +1,17 @@ +package net.mullvad.mullvadvpn.lib.model + +import android.content.Intent + +sealed interface PrepareResult + +sealed interface PrepareError : PrepareResult { + // Result from VpnService.prepare() being invoked with legacy VPN app has always-on + data object LegacyLockdown : PrepareError + + // Prepare gives intent but there is other always VPN app + data class OtherAlwaysOnApp(val appName: String) : PrepareError + + data class NotPrepared(val prepareIntent: Intent) : PrepareError +} + +data object Prepared : PrepareResult diff --git a/android/lib/resource/src/main/res/values-da/strings.xml b/android/lib/resource/src/main/res/values-da/strings.xml index 890ae1b0bc48..3aff625ee01c 100644 --- a/android/lib/resource/src/main/res/values-da/strings.xml +++ b/android/lib/resource/src/main/res/values-da/strings.xml @@ -22,7 +22,7 @@ Alle placeringer Alle udbydere Kunne ikke starte tunnelforbindelse. Deaktiver Altid-til VPN for <b>%1$s</b>. - Altid-til VPN tildelt en anden app + Altid-til VPN tildelt en anden app Enhver Administrer og tilføj brugerdefinerede metoder for at få adgang til Mullvad API. Appen skal kommunikere med en Mullvad API-server for at kunne logge dig på, hente serverlister og andre kritiske operationer. @@ -133,7 +133,6 @@ Indtast MTU Indtast kuponkode Der opstod en fejl. - KUNNE IKKE SIKRE FORBINDELSEN Ekskluderede applikationer Kan ikke blokere al netværkstrafik. Udfør fejlfinding, eller indsend en problemrapport. Kunne ikke oprette konto @@ -333,7 +332,6 @@ Det ser ud til, at du har indtastet et kontonummer i stedet for en rabatkuponkode. Hvis du vil ændre den aktive konto, skal du først logge ud. Indløsning af kuponen lykkedes. VPN-tilladelse blev nægtet, da tunnelen blev oprettet. Prøv at oprette forbindelse igen. - Altid-til VPN er måske aktiveret for en anden app VPN-tilladelsesfejl Vi vil undersøge dette. Brugerdefineret diff --git a/android/lib/resource/src/main/res/values-de/strings.xml b/android/lib/resource/src/main/res/values-de/strings.xml index c992536bb2f7..cf0751e1e6e9 100644 --- a/android/lib/resource/src/main/res/values-de/strings.xml +++ b/android/lib/resource/src/main/res/values-de/strings.xml @@ -22,7 +22,7 @@ Alle Standorte Alle Anbieter Tunnelverbindung kann nicht gestartet werden. Bitte deaktivieren Sie Always-on VPN für <b>%1$s</b>, bevor Sie Mullvad VPN verwenden. - Always-on VPN ist einer anderen App zugeordnet + Always-on VPN ist einer anderen App zugeordnet Beliebige Verwaltung und Hinzufügen benutzerdefinierter Methoden für den Zugriff auf die Mullvad-API. Die App muss mit einem Mullvad API-Server kommunizieren, um Sie anzumelden, Serverlisten abzurufen und andere wichtige Vorgänge durchzuführen. @@ -133,7 +133,6 @@ MTU eingeben Gutscheincode eingeben Ein Fehler ist aufgetreten. - SICHERE VERBINDUNG KONNTE NICHT HERGESTELLT WERDEN Ausgeschlossene Anwendungen Der Netzwerk-Traffic konnte nicht gänzlich blockiert werden. Bitte beheben Sie den Fehler oder senden Sie einen Problembericht. Konto konnte nicht erstellt werden @@ -333,7 +332,6 @@ Anscheinend haben Sie eine Kontonummer statt eines Gutscheincodes eingegeben. Wenn Sie das aktive Konto wechseln möchten, melden Sie sich bitte zuerst ab. Der Gutschein wurde erfolgreich eingelöst. VPN-Berechtigungen wurden beim Erstellen des Tunnels abgelehnt. - Always-on VPN könnte für eine andere App aktiviert sein VPN-Berechtigungsfehler Wir werden uns das anschauen. Benutzerdefiniert diff --git a/android/lib/resource/src/main/res/values-es/strings.xml b/android/lib/resource/src/main/res/values-es/strings.xml index 984c3d21cd5b..7b91a4a99926 100644 --- a/android/lib/resource/src/main/res/values-es/strings.xml +++ b/android/lib/resource/src/main/res/values-es/strings.xml @@ -22,7 +22,7 @@ Todas las ubicaciones Todos los proveedores No se puede iniciar la conexión de túnel. Deshabilite la VPN siempre activa en <b>%1$s</b> antes de utilizar la VPN de Mullvad. - La VPN siempre activa se ha asignado a otra aplicación + La VPN siempre activa se ha asignado a otra aplicación Cualquiera Gestione y añada métodos personalizados para acceder a la API de Mullvad. La aplicación necesita comunicarse con un servidor API de Mullvad para iniciar su sesión, obtener las listas de servidores y otras operaciones críticas. @@ -133,7 +133,6 @@ Introducir MTU Escriba el código del cupón Se produjo un error. - NO SE PUDO PROTEGER LA CONEXIÓN Aplicaciones excluidas No se puede bloquear todo el tráfico de red. Intente solucionar el problema o envíe un informe de problemas. No se puede crear la cuenta @@ -333,7 +332,6 @@ Parece que ha introducido un número de cuenta en lugar de un código de cupón. Si desea cambiar la cuenta activa, cierre primero la sesión. El cupón se canjeó correctamente. Se denegó el permiso para usar una conexión VPN al crear el túnel. Intente volver a establecer la conexión. - La VPN siempre activa podría estar habilitada en otra aplicación Error en la autorización de la VPN Revisaremos esto. Personalizado diff --git a/android/lib/resource/src/main/res/values-fi/strings.xml b/android/lib/resource/src/main/res/values-fi/strings.xml index 09069d2b8b72..aec745b9aa4c 100644 --- a/android/lib/resource/src/main/res/values-fi/strings.xml +++ b/android/lib/resource/src/main/res/values-fi/strings.xml @@ -22,7 +22,7 @@ Kaikki sijainnit Kaikki palveluntarjoajat Tunneliyhteyden käynnistäminen ei onnistu. Poista aina päällä oleva VPN käytöstä sovellukselle <b>%1$s</b> ennen Mullvad VPN:n käyttämistä. - Aina päällä oleva VPN on määritetty toiselle sovellukselle + Aina päällä oleva VPN on määritetty toiselle sovellukselle Mikä tahansa Hallitse ja lisää mukautettuja menetelmiä Mullvadin ohjelmointirajapinnan käyttämiseksi. Sovelluksen on kommunikoitava Mullvadin ohjelmointirajapinnan palvelimen kanssa, jotta sinut voidaan kirjata sisään, palvelinluetteloiden hakemiseksi sekä muiden tärkeiden toimintojen suorittamiseksi. @@ -133,7 +133,6 @@ Syötä MTU Syötä kuponkikoodi Ilmeni virhe. - YHTEYDEN SUOJAAMINEN EPÄONNISTUI Poissuljetut sovellukset Kaiken verkkoliikenteen estäminen ei onnistu. Käytä vianetsintää tai lähetä ongelmaraportti. Tilin luonti epäonnistui @@ -333,7 +332,6 @@ Näytät syöttäneen tilin numeron etusetelin koodin sijaan. Jos haluat vaihtaa tiliä, kirjaudu ensin ulos nykyiseltä tililtä. Kupongin lunastus onnistui. VPN-lupa evättiin tunnelia luotaessa. Yritä muodostaa yhteys uudelleen. - Aina päällä oleva VPN on mahdollisesti otettu käyttöön toiselle sovellukselle VPN-lupavirhe Tutkimme asiaa. Mukautettu diff --git a/android/lib/resource/src/main/res/values-fr/strings.xml b/android/lib/resource/src/main/res/values-fr/strings.xml index d5245f8d515e..770adcc213dc 100644 --- a/android/lib/resource/src/main/res/values-fr/strings.xml +++ b/android/lib/resource/src/main/res/values-fr/strings.xml @@ -22,7 +22,7 @@ Toutes les localisations Tous les fournisseurs Impossible de démarrer la connexion au tunnel. Veuillez désactiver « Toujours exiger un VPN « pour <b>%1$s</b> avant d\'utiliser Mullvad VPN. - « Toujours exiger un VPN » est assigné à une autre application + « Toujours exiger un VPN » est assigné à une autre application N\'importe lequel Gérez et ajoutez des modes d\'accès personnalisés à l\'API Mullvad. L\'application doit communiquer avec un serveur d\'API Mullvad pour vous connecter, récupérer des listes de serveurs et effectuer d\'autres opérations critiques. @@ -133,7 +133,6 @@ Saisir le MTU Saisir un code de bon Une erreur est survenue. - ÉCHEC DE LA SÉCURISATION DE LA CONNEXION Applications exclues Impossible de bloquer tout le trafic réseau. Veuillez dépanner ou envoyer un rapport de problème. Échec de la création du compte @@ -333,7 +332,6 @@ Vous semblez avoir saisi un numéro de compte plutôt qu\'un code de bon. Si vous souhaitez modifier le compte actif, veuillez d\'abord vous déconnecter. Le bon a bien été échangé. La permission VPN a été refusée lors de la création du tunnel. Veuillez essayer de vous reconnecter. - « Toujours exiger un VPN » est peut-être activé pour une autre application Erreur de permission VPN Nous allons nous pencher dessus. Personnalisé diff --git a/android/lib/resource/src/main/res/values-it/strings.xml b/android/lib/resource/src/main/res/values-it/strings.xml index 55ac2960dc78..9555a679874b 100644 --- a/android/lib/resource/src/main/res/values-it/strings.xml +++ b/android/lib/resource/src/main/res/values-it/strings.xml @@ -22,7 +22,7 @@ Tutti i luoghi Tutti i fornitori Impossibile avviare la connessione tunnel. Disabilita VPN sempre attiva per <b>%1$s</b> prima di utilizzare Mullvad VPN. - VPN sempre attiva assegnata a un\'altra app + VPN sempre attiva assegnata a un\'altra app Qualsiasi Gestisci e aggiungi metodi personalizzati per accedere all\'API Mullvad. L\'app deve comunicare con un server API Mullvad per accedere, recuperare elenchi di server e altre operazioni critiche. @@ -133,7 +133,6 @@ Inserisci MTU Inserisci codice voucher Si è verificato un errore. - IMPOSSIBILE STABILIRE UNA CONNESSIONE PROTETTA Applicazioni escluse Impossibile bloccare tutto il traffico di rete. Consulta la risoluzione dei problemi o invia una segnalazione del problema. Impossibile creare l\'account @@ -333,7 +332,6 @@ Sembra che tu abbia inserito un numero di account anziché un codice voucher. Se desideri modificare l\'account attivo, effettua prima la disconnessione. Il voucher è stato riscattato correttamente. L\'autorizzazione VPN è stata negata durante la creazione del tunnel. Prova a connetterti di nuovo. - La VPN sempre attiva potrebbe essere abilitata per un\'altra app Errore di autorizzazione VPN Verificheremo. Personalizzato diff --git a/android/lib/resource/src/main/res/values-ja/strings.xml b/android/lib/resource/src/main/res/values-ja/strings.xml index dd11e1b2729e..a5c422f2318e 100644 --- a/android/lib/resource/src/main/res/values-ja/strings.xml +++ b/android/lib/resource/src/main/res/values-ja/strings.xml @@ -22,7 +22,7 @@ すべての場所 すべてのプロバイダ トンネル接続を開始できません。Mullvad VPNを使用する前に<b>%1$s</b>のAlways-on VPNを無効にしてください。 - Always-on VPNは他のアプリに割り当てられています + Always-on VPNは他のアプリに割り当てられています すべて Mullvad APIへのカスタムのアクセス方法を管理・追加します。 アプリはユーザーのログイン、サーバーリストの取得、およびその他の重要な操作を行うためにMullvad APIサーバーと通信する必要があります。 @@ -133,7 +133,6 @@ MTU を入力 バウチャーコードを入力 エラー発生。 - セキュリティ保護接続を確立できませんでした 除外対象アプリケーション すべてのネットワークトラフィックをブロックできません。問題に対処するか、問題の報告を送信してください。 アカウントを作成できませんでした @@ -333,7 +332,6 @@ バウチャーコードではなくアカウント番号を入力したようです。有効なアカウントを変更する場合は、先にログアウトしてください。 バウチャーを正常に使用しました。 トンネルを作成中にVPNへのアクセスが拒否されました。もう一度接続してみてください。 - Always-on VPNが別のアプリで有効になっている可能性があります VPN許可エラー この問題を調査いたします。 カスタム diff --git a/android/lib/resource/src/main/res/values-ko/strings.xml b/android/lib/resource/src/main/res/values-ko/strings.xml index 7b1d4f9f9f55..a3a4656baa9e 100644 --- a/android/lib/resource/src/main/res/values-ko/strings.xml +++ b/android/lib/resource/src/main/res/values-ko/strings.xml @@ -22,7 +22,7 @@ 모든 위치 모든 제공업체 터널 연결을 시작할 수 없습니다. Mullvad VPN을 사용하기 전에 <b>%1$s</b>에 대한 상시 접속 VPN을 비활성화하세요. - 상시 접속 VPN이 다른 앱에 할당됨 + 상시 접속 VPN이 다른 앱에 할당됨 모두 Mullvad API에 액세스하기 위한 사용자 지정 방법을 관리하고 추가합니다. 이 앱은 로그인, 서버 목록 가져오기 및 기타 중요한 작업을 위해 Mullvad API 서버와 통신해야 합니다. @@ -133,7 +133,6 @@ MTU 입력 바우처 코드 입력 오류가 발생했습니다. - 보안 연결 실패 제외된 애플리케이션 모든 네트워크 트래픽을 차단할 수는 없습니다. 문제를 해결하거나 문제 보고서를 보내주세요. 계정을 만들지 못함 @@ -333,7 +332,6 @@ 바우처 코드 대신 계정 번호를 입력한 것 같습니다. 활성 계정을 변경하려면 먼저 로그아웃하세요. 바우처가 성공적으로 사용되었습니다. 터널을 만드는 동안 VPN 사용 권한이 거부되었습니다. 다시 연결해 보세요. - 상시 접속 VPN이 다른 앱에 활성화되었을 수 있습니다. VPN 권한 오류 조사해보겠습니다. 사용자 지정 diff --git a/android/lib/resource/src/main/res/values-my/strings.xml b/android/lib/resource/src/main/res/values-my/strings.xml index 7b3ff221eba2..2e506b2d7f5e 100644 --- a/android/lib/resource/src/main/res/values-my/strings.xml +++ b/android/lib/resource/src/main/res/values-my/strings.xml @@ -22,7 +22,7 @@ တည်နေရာအာလုံး ပံ့ပိုးသူအားလုံး Tunnel ချိတ်ဆက်မှုကို စတင်၍ မရနိုင်ပါ။ Mullvad VPN ကို မသုံးမီ <b>%1$s</b> အတွက် VPN အမြဲဖွင့်ထားမှုကို ပိတ်ပေးပါ။ - အမြဲဖွင့် VPN ကို အခြားအက်ပ်တစ်ခုသို့ သတ်မှတ်ထားပါသည် + အမြဲဖွင့် VPN ကို အခြားအက်ပ်တစ်ခုသို့ သတ်မှတ်ထားပါသည် တစ်ခုခု Mullvad API ကို ရယူသုံးစွဲရန် စိတ်ကြိုက် နည်းလမ်းများကို ပေါင်းထည့်၍ စီမံပါ။ Mullvad API ဆာဗာသို့ သင်ဝင်ရောက်ရန်၊ ဆာဗာစာရင်းများ ရယူရန်နှင့် အလွန်အရေးပါသည့် အခြားလုပ်ဆောင်မှုများအတွက် ဤအက်ပ်သည် ၎င်းနှင့်ဆက်သွယ်ရန် လိုအပ်ပါသည်။ @@ -133,7 +133,6 @@ MTU ကို ရိုက်ထည့်ရန် ဘောက်ချာကုဒ် ဖြည့်သွင်းရန် ချို့ယွင်းချက် ဖြစ်ပေါ်ခဲ့ပါသည်။ - ချိတ်ဆက်မှုကို ကာကွယ်ရန် မအောင်မြင်ပါ အပလီကေးရှင်းများ ဖယ်ထားပြီး ကွန်ရက် ကူးလူးမှု အားလုံးကို ပိတ်ဆို့၍ မရနိုင်ပါ။ ပြစ်ချက် ရှာဖွေဖယ်ရှားပေးပါ သို့မဟုတ် ပြဿနာ ရီပို့တ်တစ်ခု ပေးပို့ပါ။ အကောင့် ဖန်တီးရန် မအောင်မြင်ခဲ့ပါ @@ -333,7 +332,6 @@ ဘောက်ချာကုဒ်အစား အကောင့်နံပါတ်တစ်ခုကို ထည့်သွင်းထားပုံရသည်။ အသုံးပြုနေသောအကောင့်ကို ပြောင်းလဲလိုပါက ဦးစွာ အကောင့်မှထွက်ပါ။ ဘောက်ချာကို အောင်မြင်စွာ လဲယူခဲ့ပါသည်။ Tunnel ဖန်တီးနေစဉ် VPN ခွင့်ပြုချက်ကို ပယ်ချခဲ့ပါသည်။ ထပ်မံချိတ်ဆက်ပေးပါ။ - အမြဲဖွင့် VPN ကို နောက်ထပ်အက်ပ်အတွက် ဖွင့်ထားနိုင်ပါသည် VPN ခွင့်ပြုချက် ချို့ယွင်းချက် ဤသည်ကို စစ်ဆေးလိုက်ပါမည်။ စိတ်ကြိုက် diff --git a/android/lib/resource/src/main/res/values-nb/strings.xml b/android/lib/resource/src/main/res/values-nb/strings.xml index 600930f1a289..968abb9cf689 100644 --- a/android/lib/resource/src/main/res/values-nb/strings.xml +++ b/android/lib/resource/src/main/res/values-nb/strings.xml @@ -22,7 +22,7 @@ Alle steder Alle leverandører Kunne ikke starte tunneltilkobling. Deaktiver VPN som alltid er på, for <b>%1$s</b> før du bruker Mullvad VPN. - VPN som alltid er på, er tilordnet en annen app + VPN som alltid er på, er tilordnet en annen app Hvilken som helst Administrer og legg til tilpassede metoder for tilgang til Mullvad API. Appen må kunne kommunisere med en Mullvad API-server for å logge deg inn, hente serverlister og utføre andre kritiske operasjoner. @@ -133,7 +133,6 @@ Angi MTU Skriv inn kupongkode Det oppstod en feil. - KUNNE IKKE OPPRETTE SIKKER TILKOBLING Ekskluder applikasjoner Kunne ikke blokkere all nettverkstrafikk. Feilsøk eller send inn en problemrapport. Kunne ikke opprette konto @@ -333,7 +332,6 @@ Det ser ut til at du har oppgitt et kontonummer i stedet for en kupongkode. Hvis du vil endre den aktive kontoen, må du først logge ut. Kupongkoden er løst inn. VPN-tillatelse ble avvist under opprettelsen av tunnelen. Prøv å koble til igjen. - VPN som alltid er på, kan være aktivert for en annen app Feil med VPN-tillatelse Dette skal vi følge opp. Egendefinert diff --git a/android/lib/resource/src/main/res/values-nl/strings.xml b/android/lib/resource/src/main/res/values-nl/strings.xml index 4db93cec36ef..7f70312af1c5 100644 --- a/android/lib/resource/src/main/res/values-nl/strings.xml +++ b/android/lib/resource/src/main/res/values-nl/strings.xml @@ -22,7 +22,7 @@ Alle locaties Alle aanbieders Kan de tunnelverbinding niet starten. Schakel Altijd-aan VPN uit voor <b>%1$s</b> voordat u Mullvad VPN gebruikt. - Altijd-aan VPN toegewezen aan andere app + Altijd-aan VPN toegewezen aan andere app Elke Beheer en voeg aangepaste methoden toe om toegang te krijgen tot de Mullvad-API. De app moet communiceren met een Mullvad-API-server om u aan te melden, serverlijsten op te halen en andere belangrijke handelingen uit te voeren. @@ -133,7 +133,6 @@ Voer MTU in Vouchercode invoeren Er is een fout opgetreden. - VERBINDING BEVEILIGEN MISLUKT Uitgesloten toepassingen Kan niet alle netwerkverkeer blokkeren. Los problemen op of stuur een probleemmelding. Account aanmaken mislukt @@ -333,7 +332,6 @@ Het lijkt erop dat u een accountnummer hebt ingevoerd in plaats van een vouchercode. Als u het actieve account wilt wijzigen, meld u dan eerst af. Voucher is ingewisseld. VPN-toestemming is geweigerd tijdens maken van de tunnel. Probeer opnieuw verbinding te maken. - Altijd-aan VPN is mogelijk ingeschakeld voor een andere app VPN-machtigingsfout We gaan het bekijken. Aangepast diff --git a/android/lib/resource/src/main/res/values-pl/strings.xml b/android/lib/resource/src/main/res/values-pl/strings.xml index 080564c51ed3..9fe241fe5d4a 100644 --- a/android/lib/resource/src/main/res/values-pl/strings.xml +++ b/android/lib/resource/src/main/res/values-pl/strings.xml @@ -22,7 +22,7 @@ Wszystkie lokalizacje Wszyscy dostawcy Nie można uruchomić połączenia tunelowego. Przed rozpoczęciem użytkowania usługi Mullvad VPN wyłącz opcję „Zawsze włączony VPN” w <b>%1$s</b>. - Opcja „Zawsze włączony VPN” przypisana jest do innej aplikacji + Opcja „Zawsze włączony VPN” przypisana jest do innej aplikacji Dowolny Zarządzaj i dodawaj niestandardowe metody dostępu do interfejsu API Mullvad. Aplikacja musi komunikować się z serwerem API Mullvad, aby można było się zalogować, pobrać listy serwerów i wykonywać inne krytyczne operacje. @@ -133,7 +133,6 @@ Wprowadź MTU Wprowadź kod kuponu Wystąpił błąd. - BŁĄD ZABEZPIECZANIA POŁĄCZENIA Wykluczone aplikacje Nie można zablokować całego ruchu sieciowego. Rozwiąż problem lub wyślij zgłoszenie problemu. Nie można utworzyć konta @@ -333,7 +332,6 @@ Wygląda na to, że wpisano numer konta zamiast kodu kuponu. Jeśli chcesz zmienić aktywne konto, najpierw się wyloguj. Kupon został zrealizowany. Uprawnienie VPN zostało odrzucone podczas tworzenia tunelu. Spróbuj połączyć się ponownie. - Opcja „Zawsze włączony VPN” może być włączona dla innej aplikacji Błąd uprawnienia VPN Sprawdzimy to. Niestandardowy diff --git a/android/lib/resource/src/main/res/values-pt/strings.xml b/android/lib/resource/src/main/res/values-pt/strings.xml index d3d230a5bab0..62173cd22a21 100644 --- a/android/lib/resource/src/main/res/values-pt/strings.xml +++ b/android/lib/resource/src/main/res/values-pt/strings.xml @@ -22,7 +22,7 @@ Todas as localizações Todos os fornecedores Não foi possível iniciar a ligação de túnel. Desative a VPN sempre ligada para <b>%1$s</b> antes de utilizar a Mullvad VPN. - VPN sempre ligada atribuída a outra app + VPN sempre ligada atribuída a outra app Qualquer Gerir e adicionar métodos personalizados para aceder à Mullvad API. A app precisa de comunicar com um servidor da Mullvad API para iniciar a sua sessão, obter listas de servidores e outras operações críticas. @@ -133,7 +133,6 @@ Introduzir MTU Introduza o código do voucher Ocorreu um erro. - ERRO AO ESTABELECER LIGAÇÃO SEGURA Aplicações excluídas Não foi possível bloquear todo o tráfego de rede. Experimente a resolução de problemas ou envie um relatório do problema. Não foi possível criar a conta @@ -333,7 +332,6 @@ Parece que introduziu um número de conta em vez de um código de voucher. Se pretender alterar a conta ativa, termine a sessão primeiro. O voucher foi reclamado com sucesso. A transmissão foi negada durante a criação do túnel. Tente fazer novamente a ligação. - A VPN sempre ligada pode estar ativada para outra app Erro de permissão da VPN Vamos analisar esta situação. Personalizado diff --git a/android/lib/resource/src/main/res/values-ru/strings.xml b/android/lib/resource/src/main/res/values-ru/strings.xml index 6f59122aa745..2b1421327c7a 100644 --- a/android/lib/resource/src/main/res/values-ru/strings.xml +++ b/android/lib/resource/src/main/res/values-ru/strings.xml @@ -22,7 +22,7 @@ Все местоположения Все провайдеры Не удалось запустить туннельное подключение. Перед использованием Mullvad VPN отключите опцию «Постоянная VPN» для приложения <b>%1$s</b>. - Опция «Постоянная VPN» назначена другому приложению + Опция «Постоянная VPN» назначена другому приложению Все Добавление пользовательских методов для доступа к API Mullvad и управление ими. Приложение должно взаимодействовать с сервером API Mullvad для входа в учетную запись, получения списков серверов и других важных операций. @@ -133,7 +133,6 @@ Введите MTU Введите код ваучера Произошла ошибка. - НЕ УДАЛОСЬ УСТАНОВИТЬ БЕЗОПАСНОЕ ПОДКЛЮЧЕНИЕ Исключенные приложения Не удалось заблокировать весь сетевой трафик. Устраните неполадки или отправьте сообщение о проблеме. Не удалось создать учетную запись @@ -333,7 +332,6 @@ Вы ввели номер учетной записи вместо кода ваучера. Чтобы изменить активную учетную запись, сначала выйдите из системы. Ваучер погашен. При создании туннеля в доступе к VPN было отказано. Попробуйте подключиться снова. - Опцию «Постоянная VPN» может быть включена для другого приложения Ошибка разрешения для VPN Мы рассмотрим эту проблему. Пользовательский diff --git a/android/lib/resource/src/main/res/values-sv/strings.xml b/android/lib/resource/src/main/res/values-sv/strings.xml index f3d185c5f965..46de3c9fe259 100644 --- a/android/lib/resource/src/main/res/values-sv/strings.xml +++ b/android/lib/resource/src/main/res/values-sv/strings.xml @@ -22,7 +22,7 @@ Alla platser Alla leverantörer Det går inte att starta tunnelanslutning. Aktivera VPN som alltid är på för <b>%1$s</b> innan du använder Mullvad VPN. - VPN som alltid är på har tilldelats till annan app + VPN som alltid är på har tilldelats till annan app Valfri Hantera och lägg till anpassade metoder för att komma åt Mullvad API. Appen måste kommunicera med en Mullvad API-server för att logga in dig, hämta serverlistor och andra viktiga åtgärder. @@ -133,7 +133,6 @@ Ange MTU Ange kupongkod Ett fel har inträffat. - DET GICK INTE ATT SÄKRA ANSLUTNINGEN Exkluderade applikationer Det går inte att blockera all nätverkstrafik. Felsök eller skicka en problemrapport. Det gick inte att skapa konto @@ -333,7 +332,6 @@ Det verkar som om du angett ett kontonummer istället för en kupongkod. Logga först ut om du vill ändra den aktiva koden. Kupongen har lösts in. VPN-behörighet nekades när tunneln skapades. Försök att ansluta igen. - VPN som alltid är på kan ha aktiverats för annan app Behörighetsfel med VPN Vi kommer att undersöka detta. Anpassad diff --git a/android/lib/resource/src/main/res/values-th/strings.xml b/android/lib/resource/src/main/res/values-th/strings.xml index 3ceee70558fb..86002ddad99d 100644 --- a/android/lib/resource/src/main/res/values-th/strings.xml +++ b/android/lib/resource/src/main/res/values-th/strings.xml @@ -22,7 +22,7 @@ ตำแหน่งที่ตั้งทั้งหมด ผู้ให้บริการทั้งหมด ไม่สามารถเริ่มการเชื่อมต่ออุโมงค์ได้ โปรดปิดใช้งาน Always-on VPN เป็นเวลา <b>%1$s</b> ก่อนที่จะใช้งาน Mullvad VPN - Always-on VPN ได้รับการมอบหมายไปยังแอปอื่นแล้ว + Always-on VPN ได้รับการมอบหมายไปยังแอปอื่นแล้ว อะไรก็ได้ จัดการและเพิ่มวิธีแบบกำหนดเอง เพื่อเข้าถึง Mullvad API แอปจำเป็นต้องสื่อสารกับเซิร์ฟเวอร์ Mullvad API เพื่อนำคุณเข้าสู่ระบบ ดึงข้อมูลรายการเซิร์ฟเวอร์ และการดำเนินการที่สำคัญอื่นๆ @@ -133,7 +133,6 @@ ป้อน MTU ป้อนรหัสบัตรกำนัล เกิดข้อผิดพลาดขึ้น - ไม่สามารถเชื่อมต่ออย่างปลอดภัยได้ แอปพลิเคชันที่แยกออก ไม่สามารถบล็อกการรับส่งข้อมูลทางเครือข่ายทั้งหมดได้ โปรดแก้ไขปัญหาหรือส่งรายงานปัญหา ไม่สามารถสร้างบัญชีได้ @@ -333,7 +332,6 @@ ดูเหมือนว่า คุณได้ป้อนหมายเลขบัญชีแทนรหัสบัตรกำนัล หากคุณต้องการเปลี่ยนบัญชีที่ใช้งานอยู่ โปรดออกจากระบบก่อน แลกบัตรกำนัลสำเร็จแล้ว การให้สิทธิ์ VPN ถูกปฏิเสธ ในขณะที่สร้างอุโมงค์ โปรดลองเชื่อมต่อใหม่อีกครั้ง - Always-on VPN อาจได้รับการเปิดใช้งานสำหรับแอปอื่น เกิดข้อผิดพลาดในการอนุญาต VPN เราจะตรวจสอบปัญหานี้ กำหนดเอง diff --git a/android/lib/resource/src/main/res/values-tr/strings.xml b/android/lib/resource/src/main/res/values-tr/strings.xml index 296346242f8b..1d5f27d21513 100644 --- a/android/lib/resource/src/main/res/values-tr/strings.xml +++ b/android/lib/resource/src/main/res/values-tr/strings.xml @@ -22,7 +22,7 @@ Tüm konumlar Tüm hizmet sağlayıcılar Tünel bağlantısı başlatılamıyor. Mullvad VPN\'i kullanmadan önce lütfen Her zaman açık VPN\'i <b>%1$s</b> için devre dışı bırakın. - Her zaman açık VPN başka bir uygulamaya atandı + Her zaman açık VPN başka bir uygulamaya atandı Tümü Mullvad API\'sine erişim için özel yöntemler ekleyip yönetin. Uygulamanın oturumunuzu açmak, sunucu listelerini almak ve diğer kritik işlemleri yapmak için bir Mullvad API sunucusuyla iletişim kurması gerekir. @@ -133,7 +133,6 @@ MTU\'yu girin Kupon kodunu girin Bir hata oluştu. - GÜVENLİ BAĞLANTI OLUŞTURULAMADI Hariç tutulan uygulamalar Tüm ağ trafiği engellenemiyor. Lütfen sorunu çözmeyi deneyin veya bir hata raporu gönderin. Hesap oluşturulamadı @@ -333,7 +332,6 @@ Kupon kodu yerine hesap numarası girdiniz. Aktif hesabı değiştirmek istiyorsanız lütfen önce çıkış yapın. Kupon başarıyla kullanıldı. Tünel oluşturulurken VPN izni reddedildi. Lütfen tekrar bağlanmayı deneyin. - Her zaman açık VPN başka bir uygulama için etkinleştirilebilir VPN izin hatası Bunu araştıracağız. Özel diff --git a/android/lib/resource/src/main/res/values-zh-rCN/strings.xml b/android/lib/resource/src/main/res/values-zh-rCN/strings.xml index 82921a500160..b983b410edce 100644 --- a/android/lib/resource/src/main/res/values-zh-rCN/strings.xml +++ b/android/lib/resource/src/main/res/values-zh-rCN/strings.xml @@ -22,7 +22,7 @@ 所有位置 所有提供商 无法启动隧道连接。在使用 Mullvad VPN 之前,请为 <b>%1$s</b> 禁用“始终开启 VPN”。 - “始终开启 VPN”已分配给其他应用 + “始终开启 VPN”已分配给其他应用 任何 管理和添加访问 Mulvad API 的自定义方法。 该应用需要与 Mulvad API 服务器通信,以便您登录、获取服务器列表和执行其他关键操作。 @@ -133,7 +133,6 @@ 输入 MTU 输入优惠码 出错了。 - 无法保护连接 排除的应用程序 无法阻止所有网络流量。请排查问题或发送问题报告。 无法创建帐户 @@ -333,7 +332,6 @@ 您输入的似乎是帐号,而不是代金券码。如果您想更改有效帐户,请先退出登录。 优惠券已成功兑换。 创建隧道时,VPN 权限被拒绝。请尝试重新连接。 - 可能为另一个应用启用了“始终开启 VPN” VPN 权限错误 我们将对此进行调查。 自定义 diff --git a/android/lib/resource/src/main/res/values-zh-rTW/strings.xml b/android/lib/resource/src/main/res/values-zh-rTW/strings.xml index cb0085a0152f..d602201c47d1 100644 --- a/android/lib/resource/src/main/res/values-zh-rTW/strings.xml +++ b/android/lib/resource/src/main/res/values-zh-rTW/strings.xml @@ -22,7 +22,7 @@ 所有位置 所有供應商 無法啟動通道連線。在使用 Mullvad VPN 之前,請先為 <b>%1$s</b> 停用「始終啟用 VPN」。 - 「始終啟用 VPN」已指派給其他應用程式 + 「始終啟用 VPN」已指派給其他應用程式 任何 管理並新增自訂方式以存取 Mullvad API。 該應用程式需要與 Mulvad API 伺服器通訊,以便您登入、取得伺服器清單並執行其他重要作業。 @@ -133,7 +133,6 @@ 輸入 MTU 輸入優惠券兌換碼 發生錯誤了。 - 保護連線失敗 已排除的應用程式 無法封鎖所有網路流量。請排除故障或傳送問題回報。 無法建立帳戶 @@ -333,7 +332,6 @@ 您輸入的似乎是帳戶,而不是憑證代碼。如果您想變更有效帳戶,請先登出。 憑證已成功兌換。 建立通道時,VPN 權限被拒絕。請嘗試重新連線。 - 可能已為另一個應用程式啟用了「始終啟用 VPN」 VPN 權限錯誤 我們會對此進行調查。 自訂 diff --git a/android/lib/resource/src/main/res/values/strings.xml b/android/lib/resource/src/main/res/values/strings.xml index b89488bc1ae9..2cfb91929812 100644 --- a/android/lib/resource/src/main/res/values/strings.xml +++ b/android/lib/resource/src/main/res/values/strings.xml @@ -106,7 +106,7 @@ Edit message Try again BLOCKED CONNECTION - FAILED TO SECURE CONNECTION + FAILED TO CONNECT Connected Cancel Disconnect @@ -165,12 +165,15 @@ Hide account number Failed to remove device Changes in this version: - Always-on VPN assigned to other app + Always-on VPN assigned to %s %s before using Mullvad VPN.]]> + Always-on VPN assigned to other app + Unable to start tunnel connection. Please disable Always-on VPN before using Mullvad VPN. VPN permission error - Always-on VPN might be enabled for another app + Please press connect to request VPN permission + NEW DEVICE CREATED %s. For more details see the info button in Account.]]> diff --git a/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/ConnectionProxy.kt b/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/ConnectionProxy.kt index 2dbd15ec0360..08a0a517f08e 100644 --- a/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/ConnectionProxy.kt +++ b/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/ConnectionProxy.kt @@ -2,7 +2,6 @@ package net.mullvad.mullvadvpn.lib.shared import arrow.core.Either import arrow.core.raise.either -import arrow.core.raise.ensure import kotlinx.coroutines.flow.combine import net.mullvad.mullvadvpn.lib.daemon.grpc.ManagementService import net.mullvad.mullvadvpn.lib.model.ConnectError @@ -12,7 +11,7 @@ import net.mullvad.mullvadvpn.lib.model.TunnelState class ConnectionProxy( private val managementService: ManagementService, translationRepository: RelayLocationTranslationRepository, - private val vpnPermissionRepository: VpnPermissionRepository, + private val vpnProfileUseCase: VpnProfileUseCase, ) { val tunnelState = combine(managementService.tunnelState, translationRepository.translations) { @@ -35,7 +34,7 @@ class ConnectionProxy( copy(city = translations[city] ?: city, country = translations[country] ?: country) suspend fun connect(): Either = either { - ensure(vpnPermissionRepository.hasVpnPermission()) { ConnectError.NoVpnPermission } + vpnProfileUseCase.prepareVpn().mapLeft(ConnectError::NotPrepared).bind() managementService.connect().bind() } diff --git a/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/VpnPermissionRepository.kt b/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/VpnPermissionRepository.kt deleted file mode 100644 index b97c60316cd0..000000000000 --- a/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/VpnPermissionRepository.kt +++ /dev/null @@ -1,11 +0,0 @@ -package net.mullvad.mullvadvpn.lib.shared - -import android.content.Context -import android.net.VpnService -import net.mullvad.mullvadvpn.lib.common.util.getAlwaysOnVpnAppName - -class VpnPermissionRepository(private val applicationContext: Context) { - fun hasVpnPermission(): Boolean = VpnService.prepare(applicationContext) == null - - fun getAlwaysOnVpnAppName() = applicationContext.getAlwaysOnVpnAppName() -} diff --git a/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/VpnProfileUseCase.kt b/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/VpnProfileUseCase.kt new file mode 100644 index 000000000000..cebac0be04be --- /dev/null +++ b/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/VpnProfileUseCase.kt @@ -0,0 +1,11 @@ +package net.mullvad.mullvadvpn.lib.shared + +import android.content.Context +import arrow.core.Either +import net.mullvad.mullvadvpn.lib.common.util.prepareVpnSafe +import net.mullvad.mullvadvpn.lib.model.PrepareError +import net.mullvad.mullvadvpn.lib.model.Prepared + +class VpnProfileUseCase(private val applicationContext: Context) { + fun prepareVpn(): Either = applicationContext.prepareVpnSafe() +} diff --git a/android/lib/shared/src/test/kotlin/net/mullvad/mullvadvpn/lib/shared/ConnectionProxyTest.kt b/android/lib/shared/src/test/kotlin/net/mullvad/mullvadvpn/lib/shared/ConnectionProxyTest.kt index 065286710518..a5b46cfc2882 100644 --- a/android/lib/shared/src/test/kotlin/net/mullvad/mullvadvpn/lib/shared/ConnectionProxyTest.kt +++ b/android/lib/shared/src/test/kotlin/net/mullvad/mullvadvpn/lib/shared/ConnectionProxyTest.kt @@ -12,20 +12,20 @@ import org.junit.jupiter.api.Test class ConnectionProxyTest { private val mockManagementService: ManagementService = mockk(relaxed = true) - private val mockVpnPermissionRepository: VpnPermissionRepository = mockk() + private val mockVpnPermissionRepository: VpnProfileUseCase = mockk() private val mockTranslationRepository: RelayLocationTranslationRepository = mockk(relaxed = true) private val connectionProxy: ConnectionProxy = ConnectionProxy( managementService = mockManagementService, - vpnPermissionRepository = mockVpnPermissionRepository, + vpnProfileUseCase = mockVpnPermissionRepository, translationRepository = mockTranslationRepository, ) @Test fun `connect with vpn permission allowed should call managementService connect`() = runTest { - every { mockVpnPermissionRepository.hasVpnPermission() } returns true + every { mockVpnPermissionRepository.prepareVpn() } returns true connectionProxy.connect() coVerify(exactly = 1) { mockManagementService.connect() } } @@ -33,7 +33,7 @@ class ConnectionProxyTest { @Test fun `connect with vpn permission not allowed should not call managementService connect`() = runTest { - every { mockVpnPermissionRepository.hasVpnPermission() } returns false + every { mockVpnPermissionRepository.prepareVpn() } returns false connectionProxy.connect() coVerify(exactly = 0) { mockManagementService.connect() } } diff --git a/android/lib/talpid/build.gradle.kts b/android/lib/talpid/build.gradle.kts index c53c2add28dd..24ba625ff29a 100644 --- a/android/lib/talpid/build.gradle.kts +++ b/android/lib/talpid/build.gradle.kts @@ -30,9 +30,11 @@ android { dependencies { implementation(projects.lib.model) + implementation(projects.lib.common) implementation(libs.androidx.ktx) implementation(libs.androidx.lifecycle.service) + implementation(libs.arrow) implementation(libs.kermit) implementation(libs.kotlin.stdlib) implementation(libs.kotlinx.coroutines.android) diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/TalpidVpnService.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/TalpidVpnService.kt index dc1f8d23ca90..e608360d443c 100644 --- a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/TalpidVpnService.kt +++ b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/TalpidVpnService.kt @@ -10,6 +10,8 @@ import java.net.Inet4Address import java.net.Inet6Address import java.net.InetAddress import kotlin.properties.Delegates.observable +import net.mullvad.mullvadvpn.lib.common.util.prepareVpnSafe +import net.mullvad.mullvadvpn.lib.model.PrepareError import net.mullvad.talpid.model.CreateTunResult import net.mullvad.talpid.model.TunConfig import net.mullvad.talpid.util.TalpidSdkUtils.setMeteredIfSupported @@ -76,10 +78,17 @@ open class TalpidVpnService : LifecycleVpnService() { // Function is to be cleaned up and lint suppression to be removed. @Suppress("ReturnCount") private fun createTun(config: TunConfig): CreateTunResult { - if (prepare(this) != null) { - // VPN permission wasn't granted - return CreateTunResult.PermissionDenied - } + prepareVpnSafe() + .mapLeft { + when (it) { + is PrepareError.LegacyLockdown -> CreateTunResult.LegacyLockdown + is PrepareError.NotPrepared -> CreateTunResult.NotPrepared + is PrepareError.OtherAlwaysOnApp -> CreateTunResult.AlwaysOnApp(it.appName) + } + } + .onLeft { + return it + } val invalidDnsServerAddresses = ArrayList() diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/model/CreateTunResult.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/model/CreateTunResult.kt index 089112e3ab41..3ce8ac17740f 100644 --- a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/model/CreateTunResult.kt +++ b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/model/CreateTunResult.kt @@ -17,7 +17,13 @@ sealed class CreateTunResult { get() = true } - data object PermissionDenied : CreateTunResult() - + // Establish error data object TunnelDeviceError : CreateTunResult() + + // Prepare errors + data object LegacyLockdown : CreateTunResult() + + data class AlwaysOnApp(val appName: String) : CreateTunResult() + + data object NotPrepared : CreateTunResult() } diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/util/InetAddressExt.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/util/InetAddressExt.kt deleted file mode 100644 index d310deb884d1..000000000000 --- a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/util/InetAddressExt.kt +++ /dev/null @@ -1,10 +0,0 @@ -package net.mullvad.talpid.util - -import java.net.InetAddress - -fun InetAddress.addressString(): String { - val hostNameAndAddress = this.toString().split('/', limit = 2) - val address = hostNameAndAddress[1] - - return address -} diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/ForegroundNotificationManager.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/ForegroundNotificationManager.kt index 5745377254cb..cf324e6023b1 100644 --- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/ForegroundNotificationManager.kt +++ b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/ForegroundNotificationManager.kt @@ -2,9 +2,9 @@ package net.mullvad.mullvadvpn.service.notifications import android.app.Service import android.content.pm.ServiceInfo -import android.net.VpnService import android.os.Build import co.touchlab.kermit.Logger +import net.mullvad.mullvadvpn.lib.common.util.prepareVpnSafe import net.mullvad.mullvadvpn.lib.model.Notification import net.mullvad.mullvadvpn.lib.model.NotificationChannel import net.mullvad.mullvadvpn.lib.model.NotificationTunnelState @@ -40,7 +40,7 @@ class ForegroundNotificationManager( private fun notifyForeground(tunnelStateNotification: Notification.Tunnel) { val androidNotification = tunnelStateNotification.toNotification(vpnService) - if (VpnService.prepare(vpnService) != null) { + if (vpnService.prepareVpnSafe().isLeft()) { // Got connect/disconnect intent, but we don't have permission to go in foreground. // tunnel state will return permission and we will eventually get stopped by system. Logger.i("Did not start foreground: VPN permission not granted") @@ -65,7 +65,7 @@ class ForegroundNotificationManager( private val defaultNotification = Notification.Tunnel( NotificationChannel.TunnelUpdates.id, - NotificationTunnelState.Disconnected(true), + NotificationTunnelState.Disconnected(null), emptyList(), false, ) diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/tunnelstate/TunnelStateNotificationAction.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/tunnelstate/TunnelStateNotificationAction.kt index c66839ddc57f..f836cbcd1b68 100644 --- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/tunnelstate/TunnelStateNotificationAction.kt +++ b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/tunnelstate/TunnelStateNotificationAction.kt @@ -6,18 +6,19 @@ import android.content.Intent import androidx.core.app.NotificationCompat import net.mullvad.mullvadvpn.lib.common.constant.KEY_CONNECT_ACTION import net.mullvad.mullvadvpn.lib.common.constant.KEY_DISCONNECT_ACTION -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.common.constant.MAIN_ACTIVITY_CLASS import net.mullvad.mullvadvpn.lib.common.util.SdkUtils import net.mullvad.mullvadvpn.lib.model.Notification import net.mullvad.mullvadvpn.lib.model.NotificationAction import net.mullvad.mullvadvpn.lib.model.NotificationTunnelState +import net.mullvad.mullvadvpn.lib.model.PrepareError import net.mullvad.mullvadvpn.service.R internal fun Notification.Tunnel.toNotification(context: Context) = NotificationCompat.Builder(context, channelId.value) .setContentIntent(contentIntent(context)) - .setContentTitle(context.getString(state.contentTitleResourceId())) + .setContentTitle(state.contentTitleResourceId(context)) .setSmallIcon(R.drawable.small_logo_white) .apply { actions.forEach { addAction(it.toCompatAction(context)) } } .setOngoing(ongoing) @@ -35,37 +36,41 @@ private fun Notification.Tunnel.contentIntent(context: Context): PendingIntent { return PendingIntent.getActivity(context, 1, intent, SdkUtils.getSupportedPendingIntentFlags()) } -private fun NotificationTunnelState.contentTitleResourceId(): Int = +private fun NotificationTunnelState.contentTitleResourceId(context: Context): String = when (this) { - NotificationTunnelState.Connected -> R.string.connected - NotificationTunnelState.Connecting -> R.string.connecting + NotificationTunnelState.Connected -> context.getString(R.string.connected) + NotificationTunnelState.Connecting -> context.getString(R.string.connecting) is NotificationTunnelState.Disconnected -> { - if (this.hasVpnPermission) { - R.string.disconnected - } else { - R.string.disconnected_vpn_permission_error + when (prepareError) { + is PrepareError.NotPrepared -> + context.getString(R.string.disconnected_vpn_permission_error) + else -> context.getString(R.string.disconnected) } } - NotificationTunnelState.Disconnecting -> R.string.disconnecting - NotificationTunnelState.Reconnecting -> R.string.reconnecting - NotificationTunnelState.Error.Blocking -> R.string.blocking_internet - is NotificationTunnelState.Error.Critical -> R.string.critical_error - NotificationTunnelState.Error.DeviceOffline -> R.string.blocking_internet_device_offline + NotificationTunnelState.Disconnecting -> context.getString(R.string.disconnecting) + NotificationTunnelState.Reconnecting -> context.getString(R.string.reconnecting) + NotificationTunnelState.Error.Blocking -> context.getString(R.string.blocking_internet) + is NotificationTunnelState.Error.Critical -> context.getString(R.string.critical_error) + NotificationTunnelState.Error.DeviceOffline -> + context.getString(R.string.blocking_internet_device_offline) NotificationTunnelState.Error.VpnPermissionDenied -> - R.string.vpn_permission_error_notification_title - NotificationTunnelState.Error.AlwaysOnVpn -> R.string.always_on_vpn_error_notification_title + context.getString(R.string.vpn_permission_error_notification_title) + is NotificationTunnelState.Error.AlwaysOnVpn -> + context.getString(R.string.always_on_vpn_error_notification_title, appName) + NotificationTunnelState.Error.LegacyLockdown -> + context.getString(R.string.legacy_always_on_vpn_error_notification_title) } internal fun NotificationAction.Tunnel.toCompatAction(context: Context): NotificationCompat.Action { val pendingIntent = - if (this is NotificationAction.Tunnel.RequestPermission) { + if (this is NotificationAction.Tunnel.RequestVpnProfile) { val intent = Intent().apply { setClassName(context.packageName, MAIN_ACTIVITY_CLASS) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - setAction(KEY_REQUEST_VPN_PERMISSION) + setAction(KEY_REQUEST_VPN_PROFILE) } PendingIntent.getActivity(context, 1, intent, SdkUtils.getSupportedPendingIntentFlags()) @@ -85,7 +90,7 @@ fun NotificationAction.Tunnel.titleResource() = when (this) { NotificationAction.Tunnel.Cancel -> R.string.cancel NotificationAction.Tunnel.Connect, - NotificationAction.Tunnel.RequestPermission -> R.string.connect + is NotificationAction.Tunnel.RequestVpnProfile -> R.string.connect NotificationAction.Tunnel.Disconnect -> R.string.disconnect NotificationAction.Tunnel.Dismiss -> R.string.dismiss } @@ -93,7 +98,7 @@ fun NotificationAction.Tunnel.titleResource() = fun NotificationAction.Tunnel.toKey() = when (this) { NotificationAction.Tunnel.Connect -> KEY_CONNECT_ACTION - NotificationAction.Tunnel.RequestPermission -> KEY_REQUEST_VPN_PERMISSION + is NotificationAction.Tunnel.RequestVpnProfile -> KEY_REQUEST_VPN_PROFILE NotificationAction.Tunnel.Cancel, NotificationAction.Tunnel.Disconnect, NotificationAction.Tunnel.Dismiss -> KEY_DISCONNECT_ACTION diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/tunnelstate/TunnelStateNotificationProvider.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/tunnelstate/TunnelStateNotificationProvider.kt index 2068f1adff27..555d0ffad1ee 100644 --- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/tunnelstate/TunnelStateNotificationProvider.kt +++ b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/tunnelstate/TunnelStateNotificationProvider.kt @@ -19,15 +19,16 @@ import net.mullvad.mullvadvpn.lib.model.NotificationChannelId import net.mullvad.mullvadvpn.lib.model.NotificationId import net.mullvad.mullvadvpn.lib.model.NotificationTunnelState import net.mullvad.mullvadvpn.lib.model.NotificationUpdate +import net.mullvad.mullvadvpn.lib.model.PrepareError import net.mullvad.mullvadvpn.lib.model.TunnelState 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.lib.shared.VpnProfileUseCase import net.mullvad.mullvadvpn.service.notifications.NotificationProvider class TunnelStateNotificationProvider( connectionProxy: ConnectionProxy, - vpnPermissionRepository: VpnPermissionRepository, + vpnPermissionRepository: VpnProfileUseCase, deviceRepository: DeviceRepository, channelId: NotificationChannelId, scope: CoroutineScope, @@ -49,8 +50,7 @@ class TunnelStateNotificationProvider( tunnelState( tunnelState, actionAfterDisconnect, - vpnPermissionRepository.hasVpnPermission(), - vpnPermissionRepository.getAlwaysOnVpnAppName(), + vpnPermissionRepository.prepareVpn().leftOrNull(), ) return@combine NotificationUpdate.Notify( @@ -68,14 +68,9 @@ class TunnelStateNotificationProvider( private fun tunnelState( tunnelState: TunnelState, actionAfterDisconnect: ActionAfterDisconnect?, - hasVpnPermission: Boolean, - alwaysOnVpnPermissionName: String?, + prepareError: PrepareError?, ): NotificationTunnelState = - tunnelState.toNotificationTunnelState( - actionAfterDisconnect, - hasVpnPermission, - alwaysOnVpnPermissionName, - ) + tunnelState.toNotificationTunnelState(actionAfterDisconnect, prepareError) private fun Flow.actionAfterDisconnect(): Flow = filterIsInstance() @@ -84,11 +79,10 @@ class TunnelStateNotificationProvider( private fun TunnelState.toNotificationTunnelState( actionAfterDisconnect: ActionAfterDisconnect?, - hasVpnPermission: Boolean, - alwaysOnVpnPermissionName: String?, + prepareError: PrepareError?, ) = when (this) { - is TunnelState.Disconnected -> NotificationTunnelState.Disconnected(hasVpnPermission) + is TunnelState.Disconnected -> NotificationTunnelState.Disconnected(prepareError) is TunnelState.Connecting -> { if (actionAfterDisconnect == ActionAfterDisconnect.Reconnect) { NotificationTunnelState.Reconnecting @@ -104,20 +98,20 @@ class TunnelStateNotificationProvider( } } is TunnelState.Connected -> NotificationTunnelState.Connected - is TunnelState.Error -> toNotificationTunnelState(alwaysOnVpnPermissionName) + is TunnelState.Error -> toNotificationTunnelState() } - private fun TunnelState.Error.toNotificationTunnelState( - alwaysOnVpnPermissionName: String? - ): NotificationTunnelState.Error { + private fun TunnelState.Error.toNotificationTunnelState(): NotificationTunnelState.Error { val cause = errorState.cause return when { cause is ErrorStateCause.IsOffline && errorState.isBlocking -> NotificationTunnelState.Error.DeviceOffline cause is ErrorStateCause.InvalidDnsServers -> NotificationTunnelState.Error.Blocking - cause is ErrorStateCause.VpnPermissionDenied -> - alwaysOnVpnPermissionName?.let { NotificationTunnelState.Error.AlwaysOnVpn } - ?: NotificationTunnelState.Error.VpnPermissionDenied + cause is ErrorStateCause.LegacyLockdown -> NotificationTunnelState.Error.LegacyLockdown + cause is ErrorStateCause.NotPrepared -> + NotificationTunnelState.Error.VpnPermissionDenied + cause is ErrorStateCause.AlwaysOnApp -> + NotificationTunnelState.Error.AlwaysOnVpn(cause.appName) errorState.isBlocking -> NotificationTunnelState.Error.Blocking else -> NotificationTunnelState.Error.Critical } @@ -126,10 +120,11 @@ class TunnelStateNotificationProvider( private fun NotificationTunnelState.toAction(): NotificationAction.Tunnel = when (this) { is NotificationTunnelState.Disconnected -> { - if (this.hasVpnPermission) { - NotificationAction.Tunnel.Connect - } else { - NotificationAction.Tunnel.RequestPermission + when (prepareError) { + is PrepareError.OtherAlwaysOnApp, + is PrepareError.LegacyLockdown, + null -> NotificationAction.Tunnel.Connect + is PrepareError.NotPrepared -> NotificationAction.Tunnel.RequestVpnProfile } } NotificationTunnelState.Disconnecting -> NotificationAction.Tunnel.Connect @@ -140,6 +135,7 @@ class TunnelStateNotificationProvider( is NotificationTunnelState.Error.Critical, NotificationTunnelState.Error.DeviceOffline, NotificationTunnelState.Error.VpnPermissionDenied, - NotificationTunnelState.Error.AlwaysOnVpn -> NotificationAction.Tunnel.Dismiss + is NotificationTunnelState.Error.AlwaysOnVpn, + NotificationTunnelState.Error.LegacyLockdown -> NotificationAction.Tunnel.Dismiss } } diff --git a/android/tile/build.gradle.kts b/android/tile/build.gradle.kts index b1c558685cae..816c7de883db 100644 --- a/android/tile/build.gradle.kts +++ b/android/tile/build.gradle.kts @@ -40,6 +40,7 @@ dependencies { implementation(libs.koin.android) implementation(libs.androidx.appcompat) + implementation(libs.arrow) implementation(libs.kermit) implementation(libs.kotlin.stdlib) implementation(libs.kotlinx.coroutines.android) diff --git a/android/tile/src/main/kotlin/net/mullvad/mullvadvpn/tile/MullvadTileService.kt b/android/tile/src/main/kotlin/net/mullvad/mullvadvpn/tile/MullvadTileService.kt index b1fa9ae3119e..9fe3c06dfc7a 100644 --- a/android/tile/src/main/kotlin/net/mullvad/mullvadvpn/tile/MullvadTileService.kt +++ b/android/tile/src/main/kotlin/net/mullvad/mullvadvpn/tile/MullvadTileService.kt @@ -4,7 +4,6 @@ import android.annotation.SuppressLint import android.app.PendingIntent import android.content.Intent import android.graphics.drawable.Icon -import android.net.VpnService import android.os.Build import android.service.quicksettings.Tile import android.service.quicksettings.TileService @@ -26,6 +25,7 @@ import net.mullvad.mullvadvpn.lib.common.constant.MAIN_ACTIVITY_CLASS import net.mullvad.mullvadvpn.lib.common.constant.VPN_SERVICE_CLASS import net.mullvad.mullvadvpn.lib.common.util.SdkUtils import net.mullvad.mullvadvpn.lib.common.util.SdkUtils.setSubtitleIfSupported +import net.mullvad.mullvadvpn.lib.common.util.prepareVpnSafe import net.mullvad.mullvadvpn.lib.daemon.grpc.GrpcConnectivityState import net.mullvad.mullvadvpn.lib.daemon.grpc.ManagementService import net.mullvad.mullvadvpn.lib.model.ActionAfterDisconnect @@ -88,9 +88,26 @@ class MullvadTileService : TileService() { @SuppressLint("StartActivityAndCollapseDeprecated") private fun toggleTunnel() { - val isSetup = VpnService.prepare(applicationContext) == null + val isSetup = applicationContext.prepareVpnSafe().isRight() // TODO This logic should be more advanced, we should ensure user has an account setup etc. - if (!isSetup) { + if (isSetup) { + Logger.i("TileService: VPN service is setup") + + val intent = + Intent().apply { + setClassName(applicationContext.packageName, VPN_SERVICE_CLASS) + action = + if (qsTile.state == Tile.STATE_INACTIVE) { + KEY_CONNECT_ACTION + } else { + KEY_DISCONNECT_ACTION + } + } + + // Always start as foreground, e.g if app is dead we won't be allowed to start if not + // in foreground. + startForegroundService(intent) + } else { Logger.i("TileService: VPN service not setup, starting main activity") val intent = @@ -103,24 +120,7 @@ class MullvadTileService : TileService() { action = Intent.ACTION_MAIN } startActivityAndCollapseCompat(intent) - return - } else { - Logger.i("TileService: VPN service is setup") } - val intent = - Intent().apply { - setClassName(applicationContext.packageName, VPN_SERVICE_CLASS) - action = - if (qsTile.state == Tile.STATE_INACTIVE) { - KEY_CONNECT_ACTION - } else { - KEY_DISCONNECT_ACTION - } - } - - // Always start as foreground, e.g if app is dead we won't be allowed to start if not - // in foreground. - startForegroundService(intent) } @SuppressLint("StartActivityAndCollapseDeprecated") diff --git a/desktop/packages/mullvad-vpn/locales/messages.pot b/desktop/packages/mullvad-vpn/locales/messages.pot index 6b469ec9c573..bbcf9864a3d0 100644 --- a/desktop/packages/mullvad-vpn/locales/messages.pot +++ b/desktop/packages/mullvad-vpn/locales/messages.pot @@ -2284,10 +2284,10 @@ msgstr "" msgid "All overrides will be reset and servers IP addresses, in the Select location view, will go back to default." msgstr "" -msgid "Always-on VPN assigned to other app" +msgid "Always-on VPN assigned to %s" msgstr "" -msgid "Always-on VPN might be enabled for another app" +msgid "Always-on VPN assigned to other app" msgstr "" msgid "App info" @@ -2431,6 +2431,9 @@ msgstr "" msgid "Expand" msgstr "" +msgid "FAILED TO CONNECT" +msgstr "" + msgid "Failed to apply patch" msgstr "" @@ -2536,6 +2539,9 @@ msgstr "" msgid "Please enter a valid remote server port" msgstr "" +msgid "Please press connect to request VPN permission" +msgstr "" + msgid "Privacy" msgstr "" @@ -2644,6 +2650,9 @@ msgstr "" msgid "Unable to parse patch" msgstr "" +msgid "Unable to start tunnel connection. Please disable Always-on VPN before using Mullvad VPN." +msgstr "" + msgid "Unable to start tunnel connection. Please disable Always-on VPN for %s before using Mullvad VPN." msgstr "" diff --git a/mullvad-daemon/src/access_method.rs b/mullvad-daemon/src/access_method.rs index d4e0fc951bce..94f126a1158f 100644 --- a/mullvad-daemon/src/access_method.rs +++ b/mullvad-daemon/src/access_method.rs @@ -150,6 +150,7 @@ impl Daemon { /// different kinds of testing contexts, such as testing /// [`AccessMethodSetting`]s or on the fly testing of /// [`talpid_types::net::proxy::CustomProxy`]s. + #[cfg(not(target_os = "android"))] pub(crate) async fn test_access_method( proxy: talpid_types::net::AllowedEndpoint, access_method_selector: api::AccessModeSelectorHandle, @@ -177,6 +178,19 @@ impl Daemon { result } + #[cfg(target_os = "android")] + pub(crate) async fn test_access_method( + _: talpid_types::net::AllowedEndpoint, + _: api::AccessModeSelectorHandle, + _: crate::DaemonEventSender<( + api::AccessMethodEvent, + futures::channel::oneshot::Sender<()>, + )>, + api_proxy: ApiProxy, + ) -> Result { + Self::perform_api_request(api_proxy).await + } + /// Create an [`ApiProxy`] which will perform all REST requests against one /// specific endpoint `connection_mode`. pub fn create_limited_api_proxy(&mut self, connection_mode: ApiConnectionMode) -> ApiProxy { diff --git a/mullvad-daemon/src/api.rs b/mullvad-daemon/src/api.rs index 2558dbfee86a..016aa99ddedf 100644 --- a/mullvad-daemon/src/api.rs +++ b/mullvad-daemon/src/api.rs @@ -52,6 +52,7 @@ pub enum AccessMethodEvent { setting: AccessMethodSetting, /// The endpoint which represents how to connect to the Mullvad API and /// which clients are allowed to initiate such a connection. + #[cfg(not(target_os = "android"))] endpoint: AllowedEndpoint, }, /// Emitted when the the firewall should be updated. @@ -63,6 +64,7 @@ pub enum AccessMethodEvent { /// should be opaque to any client, it should not produce any unwanted noise /// and as such it is *not* broadcasted after the daemon is done processing /// this [`AccessMethodEvent::Allow`]. + #[cfg(not(target_os = "android"))] Allow { endpoint: AllowedEndpoint }, } @@ -419,10 +421,13 @@ impl AccessModeSelector { // created from this `AccessModeSelector` instance. As such, the // completion channel is discarded in this instance. let setting = resolved.setting.clone(); + #[cfg(not(target_os = "android"))] let endpoint = resolved.endpoint.clone(); let daemon_sender = self.access_method_event_sender.clone(); tokio::spawn(async move { - let _ = AccessMethodEvent::New { setting, endpoint } + let _ = AccessMethodEvent::New { setting, + #[cfg(not(target_os = "android"))] + endpoint } .send(daemon_sender) .await; }); diff --git a/mullvad-daemon/src/lib.rs b/mullvad-daemon/src/lib.rs index 0665c2736ebb..9edd96acc013 100644 --- a/mullvad-daemon/src/lib.rs +++ b/mullvad-daemon/src/lib.rs @@ -583,7 +583,7 @@ pub struct Daemon { relay_selector: RelaySelector, relay_list_updater: RelayListUpdaterHandle, parameters_generator: tunnel::ParametersGenerator, - shutdown_tasks: Vec + Send + Sync>>>, + shutdown_tasks: Vec + Send + Sync>>>, tunnel_state_machine_handle: TunnelStateMachineHandle, #[cfg(target_os = "windows")] volume_update_tx: mpsc::UnboundedSender<()>, @@ -631,8 +631,8 @@ impl Daemon { #[cfg(target_os = "android")] api::create_bypass_tx(&internal_event_tx), ) - .await - .map_err(Error::InitRpcFactory)?; + .await + .map_err(Error::InitRpcFactory)?; let api_availability = api_runtime.availability_handle(); api_availability.suspend(); @@ -676,8 +676,8 @@ impl Daemon { internal_event_tx.to_specialized_sender(), api_runtime.address_cache().clone(), ) - .await - .map_err(Error::ApiConnectionModeError)?; + .await + .map_err(Error::ApiConnectionModeError)?; let api_handle = api_runtime.mullvad_rest_handle(access_mode_provider); @@ -716,15 +716,15 @@ impl Daemon { .unwrap_or_default(), internal_event_tx.to_specialized_sender(), ) - .await - .map_err(Error::LoadAccountManager)?; + .await + .map_err(Error::LoadAccountManager)?; let account_history = account_history::AccountHistory::new( &settings_dir, data.device().map(|device| device.account_number.clone()), ) - .await - .map_err(Error::LoadAccountHistory)?; + .await + .map_err(Error::LoadAccountHistory)?; let target_state = if settings.auto_connect { log::info!("Automatically connecting since auto-connect is turned on"); @@ -805,8 +805,8 @@ impl Daemon { table_id: mullvad_types::TUNNEL_TABLE_ID, }, ) - .await - .map_err(Error::TunnelError)?; + .await + .map_err(Error::TunnelError)?; api::forward_offline_state(api_availability.clone(), offline_state_rx); @@ -829,7 +829,7 @@ impl Daemon { internal_event_tx.to_specialized_sender(), settings.show_beta_releases, ) - .await; + .await; // Attempt to download a fresh relay list relay_list_updater.update().await; @@ -1146,7 +1146,7 @@ impl Daemon { TunnelState::Disconnected { ref mut location, #[cfg(not(target_os = "android"))] - locked_down: _, + locked_down: _, } => *location = Some(fetched_location), TunnelState::Connected { ref mut location, .. @@ -1436,6 +1436,25 @@ impl Daemon { event: AccessMethodEvent, endpoint_active_tx: oneshot::Sender<()>, ) { + + + #[cfg(target_os = "android")] + match event { + AccessMethodEvent::New { setting, .. } => { + // Announce to all clients listening for updates of the + // currently active access method. The announcement should be + // made after the firewall policy has been updated, since the + // new access method will be useless before then. + let notifier = self.management_interface.notifier().clone(); + tokio::spawn(async move { + // Let the emitter of this event know that the firewall has been updated. + let _ = endpoint_active_tx.send(()); + // Notify clients about the change if necessary. + notifier.notify_new_access_method_event(setting); + }); + } + } + #[cfg(not(target_os = "android"))] match event { AccessMethodEvent::Allow { endpoint } => { let (completion_tx, completion_rx) = oneshot::channel(); @@ -2749,8 +2768,8 @@ impl Daemon { daemon_event_sender, api_proxy, ) - .await - .map_err(Error::AccessMethodError); + .await + .map_err(Error::AccessMethodError); Self::oneshot_send(tx, result, "on_test_proxy_as_access_method response"); }); @@ -2802,8 +2821,8 @@ impl Daemon { daemon_event_sender, api_proxy, ) - .await - .map_err(Error::AccessMethodError); + .await + .map_err(Error::AccessMethodError); log::debug!( "API access method {method} {verdict}", diff --git a/mullvad-jni/src/classes.rs b/mullvad-jni/src/classes.rs index 2dcb44156fb5..e9de44e3ad1e 100644 --- a/mullvad-jni/src/classes.rs +++ b/mullvad-jni/src/classes.rs @@ -9,7 +9,9 @@ pub const CLASSES: &[&str] = &[ "net/mullvad/talpid/model/TunConfig", "net/mullvad/talpid/model/CreateTunResult$Success", "net/mullvad/talpid/model/CreateTunResult$InvalidDnsServers", - "net/mullvad/talpid/model/CreateTunResult$PermissionDenied", + "net/mullvad/talpid/model/CreateTunResult$LegacyLockdown", + "net/mullvad/talpid/model/CreateTunResult$AlwaysOnApp", + "net/mullvad/talpid/model/CreateTunResult$NotPrepared", "net/mullvad/talpid/model/CreateTunResult$TunnelDeviceError", "net/mullvad/talpid/ConnectivityListener", "net/mullvad/talpid/TalpidVpnService", diff --git a/mullvad-management-interface/proto/management_interface.proto b/mullvad-management-interface/proto/management_interface.proto index c71c35b17ac7..53fa81f124ab 100644 --- a/mullvad-management-interface/proto/management_interface.proto +++ b/mullvad-management-interface/proto/management_interface.proto @@ -159,9 +159,16 @@ message ErrorState { CREATE_TUNNEL_DEVICE = 5; TUNNEL_PARAMETER_ERROR = 6; IS_OFFLINE = 7; - VPN_PERMISSION_DENIED = 8; - SPLIT_TUNNEL_ERROR = 9; - NEED_FULL_DISK_PERMISSIONS = 10; + // Android only + NOT_PREPARED = 8; + // Android only + ALWAYS_ON_APP = 9; + // Android only + LEGACY_LOCKDOWN = 10; + // Android only + INVALID_DNS_SERVERS = 11; + SPLIT_TUNNEL_ERROR = 12; + NEED_FULL_DISK_PERMISSIONS = 13; } enum AuthFailedError { @@ -190,6 +197,14 @@ message ErrorState { optional string lock_name = 3; } + message AlwaysOnAppError { + string app_name = 1; + } + + message InvalidDnsServersError { + repeated string ip_addrs = 1; + } + Cause cause = 1; FirewallPolicyError blocking_error = 2; @@ -201,6 +216,11 @@ message ErrorState { FirewallPolicyError policy_error = 5; // CREATE_TUNNEL_DEVICE optional int32 create_tunnel_error = 6; + + // Android only + AlwaysOnAppError always_on_app_error = 8; + // Android only + InvalidDnsServersError invalid_dns_servers_error = 9; } message TunnelState { diff --git a/mullvad-management-interface/src/types/conversions/states.rs b/mullvad-management-interface/src/types/conversions/states.rs index 4eff82f3c13e..1c8280df8829 100644 --- a/mullvad-management-interface/src/types/conversions/states.rs +++ b/mullvad-management-interface/src/types/conversions/states.rs @@ -110,12 +110,20 @@ impl From for proto::TunnelState { i32::from(Cause::IsOffline) } #[cfg(target_os = "android")] - talpid_tunnel::ErrorStateCause::VpnPermissionDenied => { - i32::from(Cause::VpnPermissionDenied) + talpid_tunnel::ErrorStateCause::NotPrepared { .. } => { + i32::from(Cause::NotPrepared) + } + #[cfg(target_os = "android")] + talpid_tunnel::ErrorStateCause::AlwaysOnApp { .. } => { + i32::from(Cause::AlwaysOnApp) + } + #[cfg(target_os = "android")] + talpid_tunnel::ErrorStateCause::LegacyLockdown => { + i32::from(Cause::LegacyLockdown) } #[cfg(target_os = "android")] talpid_tunnel::ErrorStateCause::InvalidDnsServers(_) => { - i32::from(Cause::SetDnsError) + i32::from(Cause::InvalidDnsServers) } #[cfg(any( target_os = "windows", @@ -131,43 +139,58 @@ impl From for proto::TunnelState { } }, blocking_error: error_state.block_failure().map(map_firewall_error), + // TODO Add more logic to add data thingy + always_on_app_error: if let talpid_tunnel::ErrorStateCause::AlwaysOnApp { app_name } = + error_state.cause() + { + Some(proto::error_state::AlwaysOnAppError { app_name: app_name.to_string() }) + } else { + None + }, + invalid_dns_servers_error: if let talpid_tunnel::ErrorStateCause::InvalidDnsServers(ip_addrs) = + error_state.cause() + { + Some(proto::error_state::InvalidDnsServersError { ip_addrs: ip_addrs.iter().map(|ip| ip.to_string()).collect() }) + } else { + None + }, auth_failed_error: mullvad_types::auth_failed::AuthFailed::try_from( error_state.cause(), ) - .ok() - .map(|auth_failed| { - i32::from(proto::error_state::AuthFailedError::from(auth_failed)) - }) - .unwrap_or(0i32), + .ok() + .map(|auth_failed| { + i32::from(proto::error_state::AuthFailedError::from(auth_failed)) + }) + .unwrap_or(0i32), parameter_error: - if let talpid_tunnel::ErrorStateCause::TunnelParameterError(reason) = - error_state.cause() - { - match reason { - talpid_tunnel::ParameterGenerationError::NoMatchingRelay => { - i32::from(GenerationError::NoMatchingRelay) - } - talpid_tunnel::ParameterGenerationError::NoMatchingBridgeRelay => { - i32::from(GenerationError::NoMatchingBridgeRelay) - } - talpid_tunnel::ParameterGenerationError::NoWireguardKey => { - i32::from(GenerationError::NoWireguardKey) - } - talpid_tunnel::ParameterGenerationError::CustomTunnelHostResultionError => { - i32::from(GenerationError::CustomTunnelHostResolutionError) + if let talpid_tunnel::ErrorStateCause::TunnelParameterError(reason) = + error_state.cause() + { + match reason { + talpid_tunnel::ParameterGenerationError::NoMatchingRelay => { + i32::from(GenerationError::NoMatchingRelay) + } + talpid_tunnel::ParameterGenerationError::NoMatchingBridgeRelay => { + i32::from(GenerationError::NoMatchingBridgeRelay) + } + talpid_tunnel::ParameterGenerationError::NoWireguardKey => { + i32::from(GenerationError::NoWireguardKey) + } + talpid_tunnel::ParameterGenerationError::CustomTunnelHostResultionError => { + i32::from(GenerationError::CustomTunnelHostResolutionError) + } } - } - } else { - 0 - }, + } else { + 0 + }, policy_error: - if let talpid_tunnel::ErrorStateCause::SetFirewallPolicyError(reason) = - error_state.cause() - { - Some(map_firewall_error(reason)) - } else { - None - }, + if let talpid_tunnel::ErrorStateCause::SetFirewallPolicyError(reason) = + error_state.cause() + { + Some(map_firewall_error(reason)) + } else { + None + }, #[cfg(not(target_os = "windows"))] create_tunnel_error: None, #[cfg(target_os = "windows")] @@ -230,9 +253,9 @@ impl TryFrom for mullvad_types::states::TunnelState { let state = match state.state { #[cfg_attr(target_os = "android", allow(unused_variables))] Some(proto::tunnel_state::State::Disconnected(proto::tunnel_state::Disconnected { - disconnected_location, - locked_down, - })) => MullvadState::Disconnected { + disconnected_location, + locked_down, + })) => MullvadState::Disconnected { location: disconnected_location .map(mullvad_types::location::GeoIpLocation::try_from) .transpose()?, @@ -240,13 +263,13 @@ impl TryFrom for mullvad_types::states::TunnelState { locked_down, }, Some(proto::tunnel_state::State::Connecting(proto::tunnel_state::Connecting { - relay_info: - Some(proto::TunnelStateRelayInfo { - tunnel_endpoint: Some(tunnel_endpoint), - location, - }), - feature_indicators, - })) => MullvadState::Connecting { + relay_info: + Some(proto::TunnelStateRelayInfo { + tunnel_endpoint: Some(tunnel_endpoint), + location, + }), + feature_indicators, + })) => MullvadState::Connecting { endpoint: talpid_net::TunnelEndpoint::try_from(tunnel_endpoint)?, location: location .map(mullvad_types::location::GeoIpLocation::try_from) @@ -258,13 +281,13 @@ impl TryFrom for mullvad_types::states::TunnelState { ))?, }, Some(proto::tunnel_state::State::Connected(proto::tunnel_state::Connected { - relay_info: - Some(proto::TunnelStateRelayInfo { - tunnel_endpoint: Some(tunnel_endpoint), - location, - }), - feature_indicators, - })) => MullvadState::Connected { + relay_info: + Some(proto::TunnelStateRelayInfo { + tunnel_endpoint: Some(tunnel_endpoint), + location, + }), + feature_indicators, + })) => MullvadState::Connected { endpoint: talpid_net::TunnelEndpoint::try_from(tunnel_endpoint)?, location: location .map(mullvad_types::location::GeoIpLocation::try_from) @@ -276,8 +299,8 @@ impl TryFrom for mullvad_types::states::TunnelState { ))?, }, Some(proto::tunnel_state::State::Disconnecting( - proto::tunnel_state::Disconnecting { after_disconnect }, - )) => MullvadState::Disconnecting( + proto::tunnel_state::Disconnecting { after_disconnect }, + )) => MullvadState::Disconnecting( match proto::AfterDisconnect::try_from(after_disconnect) { Ok(proto::AfterDisconnect::Nothing) => { talpid_tunnel::ActionAfterDisconnect::Nothing @@ -296,16 +319,17 @@ impl TryFrom for mullvad_types::states::TunnelState { }, ), Some(proto::tunnel_state::State::Error(proto::tunnel_state::Error { - error_state: - Some(proto::ErrorState { - cause, - blocking_error, - auth_failed_error, - parameter_error, - policy_error, - create_tunnel_error, - }), - })) => { + error_state: + Some(proto::ErrorState { + cause, + blocking_error, + auth_failed_error, + parameter_error, + policy_error, + create_tunnel_error, + .. + }), + })) => { #[cfg(not(target_os = "windows"))] let _ = create_tunnel_error; @@ -357,10 +381,6 @@ impl TryFrom for mullvad_types::states::TunnelState { }; talpid_tunnel::ErrorStateCause::TunnelParameterError(parameter_error) } - #[cfg(target_os = "android")] - Ok(proto::error_state::Cause::VpnPermissionDenied) => { - talpid_tunnel::ErrorStateCause::VpnPermissionDenied - } #[cfg(any(target_os = "windows", target_os = "macos"))] Ok(proto::error_state::Cause::SplitTunnelError) => { talpid_tunnel::ErrorStateCause::SplitTunnelError diff --git a/talpid-core/src/tunnel_state_machine/connected_state.rs b/talpid-core/src/tunnel_state_machine/connected_state.rs index b2ebd7a3feaa..33d797377f5c 100644 --- a/talpid-core/src/tunnel_state_machine/connected_state.rs +++ b/talpid-core/src/tunnel_state_machine/connected_state.rs @@ -13,6 +13,7 @@ use futures::{ stream::Fuse, StreamExt, }; +use talpid_tunnel::tun_provider::Error; use talpid_types::{ net::{AllowedClients, AllowedEndpoint, TunnelParameters}, tunnel::{ErrorStateCause, FirewallPolicyError}, @@ -25,7 +26,7 @@ use crate::tunnel::TunnelMonitor; use super::connecting_state::TunnelCloseEvent; pub(crate) type TunnelEventsReceiver = - Fuse)>>; +Fuse)>>; /// The tunnel is up and working. pub struct ConnectedState { @@ -283,6 +284,7 @@ impl ConnectedState { let _ = complete_tx.send(()); consequence } + #[cfg(not(target_os = "android"))] Some(TunnelCommand::AllowEndpoint(endpoint, tx)) => { shared_values.allowed_endpoint = endpoint; let _ = tx.send(()); @@ -293,10 +295,20 @@ impl ConnectedState { #[cfg(target_os = "android")] { if let Err(_err) = shared_values.restart_tunnel(false) { - self.disconnect( - shared_values, - AfterDisconnect::Block(ErrorStateCause::StartTunnelError), - ) + match _err { + Error::InvalidDnsServers(ip_addrs) => { + self.disconnect( + shared_values, + AfterDisconnect::Block(ErrorStateCause::InvalidDnsServers(ip_addrs)), + ) + } + _ => { + self.disconnect( + shared_values, + AfterDisconnect::Block(ErrorStateCause::StartTunnelError), + ) + } + } } else { self.disconnect(shared_values, AfterDisconnect::Reconnect(0)) } diff --git a/talpid-core/src/tunnel_state_machine/connecting_state.rs b/talpid-core/src/tunnel_state_machine/connecting_state.rs index 6739cb4402b5..36e862bddf44 100644 --- a/talpid-core/src/tunnel_state_machine/connecting_state.rs +++ b/talpid-core/src/tunnel_state_machine/connecting_state.rs @@ -267,14 +267,34 @@ impl ConnectingState { log::error!("{}", error.display_chain_with_msg("Failed to start tunnel")); let block_reason = match error { tunnel::Error::EnableIpv6Error => ErrorStateCause::Ipv6Unavailable, + #[cfg(target_os = "android")] tunnel::Error::WireguardTunnelMonitoringError( talpid_wireguard::Error::TunnelError( talpid_wireguard::TunnelError::SetupTunnelDevice( - tun_provider::Error::PermissionDenied, + tun_provider::Error::LegacyLockdown, ), ), - ) => ErrorStateCause::VpnPermissionDenied, + ) => ErrorStateCause::LegacyLockdown, + + #[cfg(target_os = "android")] + tunnel::Error::WireguardTunnelMonitoringError( + talpid_wireguard::Error::TunnelError( + talpid_wireguard::TunnelError::SetupTunnelDevice( + tun_provider::Error::AlwaysOnApp { app_name }, + ), + ), + ) => ErrorStateCause::AlwaysOnApp { app_name}, + + #[cfg(target_os = "android")] + tunnel::Error::WireguardTunnelMonitoringError( + talpid_wireguard::Error::TunnelError( + talpid_wireguard::TunnelError::SetupTunnelDevice( + tun_provider::Error::NotPrepared, + ), + ), + ) => ErrorStateCause::NotPrepared, + #[cfg(target_os = "android")] tunnel::Error::WireguardTunnelMonitoringError( talpid_wireguard::Error::TunnelError( @@ -435,6 +455,7 @@ impl ConnectingState { let _ = complete_tx.send(()); consequence } + #[cfg(not(target_os = "android"))] Some(TunnelCommand::AllowEndpoint(endpoint, tx)) => { if shared_values.allowed_endpoint != endpoint { shared_values.allowed_endpoint = endpoint; diff --git a/talpid-core/src/tunnel_state_machine/disconnected_state.rs b/talpid-core/src/tunnel_state_machine/disconnected_state.rs index 12e9c5aaa284..de1d4822cbb4 100644 --- a/talpid-core/src/tunnel_state_machine/disconnected_state.rs +++ b/talpid-core/src/tunnel_state_machine/disconnected_state.rs @@ -173,6 +173,7 @@ impl TunnelState for DisconnectedState { let _ = complete_tx.send(()); SameState(self) } + #[cfg(not(target_os = "android"))] Some(TunnelCommand::AllowEndpoint(endpoint, tx)) => { if shared_values.allowed_endpoint != endpoint { shared_values.allowed_endpoint = endpoint; diff --git a/talpid-core/src/tunnel_state_machine/disconnecting_state.rs b/talpid-core/src/tunnel_state_machine/disconnecting_state.rs index b8d43ac7e9a9..781ccaeedec8 100644 --- a/talpid-core/src/tunnel_state_machine/disconnecting_state.rs +++ b/talpid-core/src/tunnel_state_machine/disconnecting_state.rs @@ -45,6 +45,7 @@ impl DisconnectingState { let _ = complete_tx.send(()); AfterDisconnect::Nothing } + #[cfg(not(target_os = "android"))] Some(TunnelCommand::AllowEndpoint(endpoint, tx)) => { shared_values.allowed_endpoint = endpoint; let _ = tx.send(()); @@ -100,6 +101,8 @@ impl DisconnectingState { let _ = complete_tx.send(()); AfterDisconnect::Block(reason) } + + #[cfg(not(target_os = "android"))] Some(TunnelCommand::AllowEndpoint(endpoint, tx)) => { shared_values.allowed_endpoint = endpoint; let _ = tx.send(()); @@ -159,6 +162,7 @@ impl DisconnectingState { let _ = complete_tx.send(()); AfterDisconnect::Reconnect(retry_attempt) } + #[cfg(not(target_os = "android"))] Some(TunnelCommand::AllowEndpoint(endpoint, tx)) => { shared_values.allowed_endpoint = endpoint; let _ = tx.send(()); diff --git a/talpid-core/src/tunnel_state_machine/error_state.rs b/talpid-core/src/tunnel_state_machine/error_state.rs index a9ba470bfbd3..bdd8e9a014f8 100644 --- a/talpid-core/src/tunnel_state_machine/error_state.rs +++ b/talpid-core/src/tunnel_state_machine/error_state.rs @@ -4,12 +4,13 @@ use super::{ }; #[cfg(target_os = "macos")] use crate::dns::DnsConfig; +#[cfg(not(target_os = "android"))] use crate::firewall::FirewallPolicy; use futures::StreamExt; #[cfg(target_os = "macos")] use std::net::Ipv4Addr; use talpid_types::{ - tunnel::{self as talpid_tunnel, ErrorStateCause, FirewallPolicyError}, + tunnel::{ErrorStateCause, FirewallPolicyError}, ErrorExt, }; @@ -66,13 +67,14 @@ impl ErrorState { Box::new(ErrorState { block_reason: block_reason.clone(), }), - TunnelStateTransition::Error(talpid_tunnel::ErrorState::new( + TunnelStateTransition::Error(talpid_types::tunnel::ErrorState::new( block_reason, block_failure, )), ) } + #[cfg(not(target_os = "android"))] fn set_firewall_policy( shared_values: &mut SharedTunnelStateValues, ) -> Result<(), FirewallPolicyError> { @@ -145,19 +147,11 @@ impl TunnelState for ErrorState { let _ = complete_tx.send(()); consequence } + #[cfg(not(target_os = "android"))] Some(TunnelCommand::AllowEndpoint(endpoint, tx)) => { if shared_values.allowed_endpoint != endpoint { shared_values.allowed_endpoint = endpoint; let _ = Self::set_firewall_policy(shared_values); - - #[cfg(target_os = "android")] - if let Err(_err) = shared_values.restart_tunnel(true) { - let _ = tx.send(()); - return NewState(Self::enter( - shared_values, - ErrorStateCause::SetFirewallPolicyError(FirewallPolicyError::Generic), - )); - } } let _ = tx.send(()); SameState(self) @@ -168,7 +162,11 @@ impl TunnelState for ErrorState { { // DNS is blocked in the error state, so only update tun config shared_values.prepare_tun_config(true); - SameState(self) + if let ErrorStateCause::InvalidDnsServers(_) = self.block_reason { + NewState(ConnectingState::enter(shared_values, 0)) + } else { + SameState(self) + } } #[cfg(not(target_os = "android"))] { diff --git a/talpid-core/src/tunnel_state_machine/mod.rs b/talpid-core/src/tunnel_state_machine/mod.rs index 2541bc88e614..57ee4e6ecbc3 100644 --- a/talpid-core/src/tunnel_state_machine/mod.rs +++ b/talpid-core/src/tunnel_state_machine/mod.rs @@ -192,6 +192,7 @@ pub enum TunnelCommand { /// Endpoint that should never be blocked. `()` is sent to the /// channel after attempting to set the firewall policy, regardless /// of whether it succeeded. + #[cfg(not(target_os = "android"))] AllowEndpoint(AllowedEndpoint, oneshot::Sender<()>), /// Set DNS configuration to use. Dns(crate::dns::DnsConfig, oneshot::Sender<()>), diff --git a/talpid-tunnel/src/tun_provider/android/mod.rs b/talpid-tunnel/src/tun_provider/android/mod.rs index 21eb1afb1e52..8db79abe2701 100644 --- a/talpid-tunnel/src/tun_provider/android/mod.rs +++ b/talpid-tunnel/src/tun_provider/android/mod.rs @@ -46,8 +46,14 @@ pub enum Error { #[error("Failed to create tunnel device")] TunnelDeviceError, - #[error("Permission denied when trying to create tunnel")] - PermissionDenied, + #[error("Profile for VPN has not been setup")] + NotPrepared, + + #[error("Another legacy VPN profile is used as always on")] + LegacyLockdown, + + #[error("Another VPN app is used as always on")] + AlwaysOnApp { app_name: String }, } /// Factory of tunnel devices on Android. @@ -300,7 +306,7 @@ impl VpnServiceConfig { .collect() } - fn allowed_lan_networks() -> impl Iterator { + fn allowed_lan_networks() -> impl Iterator { ALLOWED_LAN_NETS .iter() .chain(ALLOWED_LAN_MULTICAST_NETS.iter()) @@ -375,8 +381,10 @@ impl AsRawFd for VpnServiceTun { enum CreateTunResult { Success { tun_fd: i32 }, InvalidDnsServers { addresses: Vec }, - PermissionDenied, TunnelDeviceError, + LegacyLockdown, + AlwaysOnApp { app_name: String }, + NotPrepared, } impl From for Result { @@ -386,7 +394,9 @@ impl From for Result { CreateTunResult::InvalidDnsServers { addresses } => { Err(Error::InvalidDnsServers(addresses)) } - CreateTunResult::PermissionDenied => Err(Error::PermissionDenied), + CreateTunResult::LegacyLockdown => Err(Error::LegacyLockdown), + CreateTunResult::NotPrepared => Err(Error::NotPrepared), + CreateTunResult::AlwaysOnApp { app_name } => Err(Error::AlwaysOnApp { app_name }), CreateTunResult::TunnelDeviceError => Err(Error::TunnelDeviceError), } } diff --git a/talpid-types/src/tunnel.rs b/talpid-types/src/tunnel.rs index 598501929427..ebf9135e65dc 100644 --- a/talpid-types/src/tunnel.rs +++ b/talpid-types/src/tunnel.rs @@ -96,9 +96,12 @@ pub enum ErrorStateCause { TunnelParameterError(ParameterGenerationError), /// This device is offline, no tunnels can be established. IsOffline, - /// The Android VPN permission was denied. #[cfg(target_os = "android")] - VpnPermissionDenied, + NotPrepared, + #[cfg(target_os = "android")] + AlwaysOnApp { app_name: String }, + #[cfg(target_os = "android")] + LegacyLockdown, /// Error reported by split tunnel module. #[cfg(any(target_os = "windows", target_os = "macos", target_os = "android"))] SplitTunnelError, @@ -204,12 +207,16 @@ impl fmt::Display for ErrorStateCause { return write!(f, "Failure to generate tunnel parameters: {err}"); } IsOffline => "This device is offline, no tunnels can be established", - #[cfg(target_os = "android")] - VpnPermissionDenied => "The Android VPN permission was denied when creating the tunnel", #[cfg(any(target_os = "windows", target_os = "macos", target_os = "android"))] SplitTunnelError => "The split tunneling module reported an error", #[cfg(target_os = "macos")] NeedFullDiskPermissions => "Need full disk access to enable split tunneling", + #[cfg(target_os = "android")] + NotPrepared => "This device is not prepared", + #[cfg(target_os = "android")] + AlwaysOnApp { app_name: _ } => "Another app is set as always on", + #[cfg(target_os = "android")] + LegacyLockdown => "Another legacy vpn profile is set as always on", }; write!(f, "{description}")