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