From 9e9682f4e0471355ed932a28515189421285293b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20G=C3=B6ransson?= Date: Mon, 18 Dec 2023 12:12:17 +0100 Subject: [PATCH] Support new tunnel state API in the Android frontend. Remove `get_current_location` from jni. --- .../compose/screen/ConnectScreenTest.kt | 71 ++------- .../compose/component/ConnectionStatusText.kt | 2 +- .../compose/screen/ConnectScreen.kt | 11 +- .../compose/screen/OutOfTimeScreen.kt | 2 +- .../compose/state/ConnectUiState.kt | 6 +- .../compose/state/OutOfTimeUiState.kt | 2 +- .../compose/state/WelcomeUiState.kt | 2 +- .../ui/serviceconnection/ConnectionProxy.kt | 4 +- .../ui/serviceconnection/LocationInfoCache.kt | 26 ---- .../ServiceConnectionContainer.kt | 2 - .../usecase/TunnelStateNotificationUseCase.kt | 2 +- .../mullvadvpn/viewmodel/ConnectViewModel.kt | 44 +++--- .../usecase/OutOfTimeUseCaseTest.kt | 2 +- .../TunnelStateNotificationUseCaseTest.kt | 2 +- .../viewmodel/ConnectViewModelTest.kt | 49 ++---- .../viewmodel/DeviceRevokedViewModelTest.kt | 2 +- .../viewmodel/OutOfTimeViewModelTest.kt | 3 +- .../viewmodel/WelcomeViewModelTest.kt | 2 +- .../net/mullvad/mullvadvpn/lib/ipc/Event.kt | 3 - .../mullvad/mullvadvpn/model/TunnelState.kt | 72 ++------- .../mullvadvpn/service/MullvadDaemon.kt | 11 +- .../mullvadvpn/service/MullvadVpnService.kt | 2 +- .../service/endpoint/ConnectionProxy.kt | 2 +- .../service/endpoint/LocationInfoCache.kt | 139 ------------------ .../service/endpoint/ServiceEndpoint.kt | 3 - .../notifications/TunnelStateNotification.kt | 2 +- .../service/util/ExponentialBackoff.kt | 52 ------- .../mullvadvpn/tile/ServiceConnection.kt | 2 +- mullvad-jni/src/daemon_interface.rs | 8 - mullvad-jni/src/lib.rs | 28 ---- 30 files changed, 82 insertions(+), 476 deletions(-) delete mode 100644 android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/LocationInfoCache.kt delete mode 100644 android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/LocationInfoCache.kt delete mode 100644 android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/util/ExponentialBackoff.kt 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 cd25c8ce0b4b..7194ed152135 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 @@ -79,7 +79,6 @@ class ConnectScreenTest { inAddress = null, outAddress = "", showLocation = false, - isTunnelInfoExpanded = false, deviceName = "", daysLeftUntilExpiry = null, inAppNotification = InAppNotification.TunnelStateBlocked @@ -114,7 +113,6 @@ class ConnectScreenTest { inAddress = null, outAddress = "", showLocation = false, - isTunnelInfoExpanded = false, deviceName = "", daysLeftUntilExpiry = null, inAppNotification = InAppNotification.TunnelStateBlocked @@ -147,7 +145,6 @@ class ConnectScreenTest { inAddress = null, outAddress = "", showLocation = false, - isTunnelInfoExpanded = false, deviceName = "", daysLeftUntilExpiry = null, inAppNotification = null @@ -179,7 +176,6 @@ class ConnectScreenTest { inAddress = null, outAddress = "", showLocation = false, - isTunnelInfoExpanded = false, deviceName = "", daysLeftUntilExpiry = null, inAppNotification = null @@ -212,7 +208,6 @@ class ConnectScreenTest { inAddress = null, outAddress = "", showLocation = true, - isTunnelInfoExpanded = false, deviceName = "", daysLeftUntilExpiry = null, inAppNotification = null @@ -240,12 +235,11 @@ class ConnectScreenTest { ConnectUiState( location = null, relayLocation = mockRelayLocation, - tunnelUiState = TunnelState.Disconnected, - tunnelRealState = TunnelState.Disconnected, + tunnelUiState = TunnelState.Disconnected(), + tunnelRealState = TunnelState.Disconnected(), inAddress = null, outAddress = "", showLocation = true, - isTunnelInfoExpanded = false, deviceName = "", daysLeftUntilExpiry = null, inAppNotification = null @@ -280,7 +274,6 @@ class ConnectScreenTest { inAddress = null, outAddress = "", showLocation = true, - isTunnelInfoExpanded = false, deviceName = "", daysLeftUntilExpiry = null, inAppNotification = @@ -319,7 +312,6 @@ class ConnectScreenTest { inAddress = null, outAddress = "", showLocation = true, - isTunnelInfoExpanded = false, deviceName = "", daysLeftUntilExpiry = null, inAppNotification = @@ -355,7 +347,6 @@ class ConnectScreenTest { inAddress = null, outAddress = "", showLocation = false, - isTunnelInfoExpanded = false, deviceName = "", daysLeftUntilExpiry = null, inAppNotification = InAppNotification.TunnelStateBlocked @@ -390,7 +381,6 @@ class ConnectScreenTest { inAddress = null, outAddress = "", showLocation = true, - isTunnelInfoExpanded = false, deviceName = "", daysLeftUntilExpiry = null, inAppNotification = InAppNotification.TunnelStateBlocked @@ -420,12 +410,11 @@ class ConnectScreenTest { ConnectUiState( location = null, relayLocation = mockRelayLocation, - tunnelUiState = TunnelState.Disconnected, - tunnelRealState = TunnelState.Disconnected, + tunnelUiState = TunnelState.Disconnected(), + tunnelRealState = TunnelState.Disconnected(), inAddress = null, outAddress = "", showLocation = false, - isTunnelInfoExpanded = false, deviceName = "", daysLeftUntilExpiry = null, inAppNotification = null @@ -457,7 +446,6 @@ class ConnectScreenTest { inAddress = null, outAddress = "", showLocation = false, - isTunnelInfoExpanded = false, deviceName = "", daysLeftUntilExpiry = null, inAppNotification = null @@ -489,7 +477,6 @@ class ConnectScreenTest { inAddress = null, outAddress = "", showLocation = false, - isTunnelInfoExpanded = false, deviceName = "", daysLeftUntilExpiry = null, inAppNotification = null @@ -515,12 +502,11 @@ class ConnectScreenTest { ConnectUiState( location = null, relayLocation = null, - tunnelUiState = TunnelState.Disconnected, - tunnelRealState = TunnelState.Disconnected, + tunnelUiState = TunnelState.Disconnected(), + tunnelRealState = TunnelState.Disconnected(), inAddress = null, outAddress = "", showLocation = false, - isTunnelInfoExpanded = false, deviceName = "", daysLeftUntilExpiry = null, inAppNotification = null @@ -551,7 +537,6 @@ class ConnectScreenTest { inAddress = null, outAddress = "", showLocation = false, - isTunnelInfoExpanded = false, deviceName = "", daysLeftUntilExpiry = null, inAppNotification = null @@ -567,38 +552,6 @@ class ConnectScreenTest { verify { mockedClickHandler.invoke() } } - @Test - fun testToggleTunnelInfo() { - // Arrange - val mockedClickHandler: () -> Unit = mockk(relaxed = true) - val dummyLocation = GeoIpLocation(null, null, "dummy country", null, "dummy hostname") - composeTestRule.setContentWithTheme { - ConnectScreen( - uiState = - ConnectUiState( - location = dummyLocation, - relayLocation = null, - tunnelUiState = TunnelState.Connecting(null, null), - tunnelRealState = TunnelState.Connecting(null, null), - inAddress = null, - outAddress = "", - showLocation = false, - isTunnelInfoExpanded = false, - deviceName = "", - daysLeftUntilExpiry = null, - inAppNotification = null - ), - onToggleTunnelInfo = mockedClickHandler - ) - } - - // Act - composeTestRule.onNodeWithTag(LOCATION_INFO_TEST_TAG).performClick() - - // Assert - verify { mockedClickHandler.invoke() } - } - @Test fun showLocationInfo() { // Arrange @@ -622,7 +575,6 @@ class ConnectScreenTest { inAddress = mockInAddress, outAddress = mockOutAddress, showLocation = false, - isTunnelInfoExpanded = true, deviceName = "", daysLeftUntilExpiry = null, inAppNotification = null @@ -630,6 +582,8 @@ class ConnectScreenTest { ) } + composeTestRule.onNodeWithTag(LOCATION_INFO_TEST_TAG).performClick() + // Assert composeTestRule.apply { onNodeWithText(mockHostName).assertExists() @@ -660,7 +614,6 @@ class ConnectScreenTest { inAddress = null, outAddress = "", showLocation = false, - isTunnelInfoExpanded = false, deviceName = "", daysLeftUntilExpiry = null, inAppNotification = InAppNotification.UpdateAvailable(versionInfo) @@ -696,7 +649,6 @@ class ConnectScreenTest { inAddress = null, outAddress = "", showLocation = false, - isTunnelInfoExpanded = false, deviceName = "", daysLeftUntilExpiry = null, inAppNotification = InAppNotification.UnsupportedVersion(versionInfo) @@ -729,7 +681,6 @@ class ConnectScreenTest { inAddress = null, outAddress = "", showLocation = false, - isTunnelInfoExpanded = false, deviceName = "", daysLeftUntilExpiry = null, inAppNotification = InAppNotification.AccountExpiry(expiryDate) @@ -767,7 +718,6 @@ class ConnectScreenTest { inAddress = null, outAddress = "", showLocation = false, - isTunnelInfoExpanded = false, deviceName = "", daysLeftUntilExpiry = null, inAppNotification = InAppNotification.UnsupportedVersion(versionInfo) @@ -799,7 +749,6 @@ class ConnectScreenTest { inAddress = null, outAddress = "", showLocation = false, - isTunnelInfoExpanded = false, deviceName = "", daysLeftUntilExpiry = null, inAppNotification = InAppNotification.AccountExpiry(expiryDate) @@ -816,10 +765,8 @@ class ConnectScreenTest { @Test fun testOpenAccountView() { - - val onAccountClickMockk: () -> Unit = mockk(relaxed = true) - // Arrange + val onAccountClickMockk: () -> Unit = mockk(relaxed = true) composeTestRule.setContentWithTheme { ConnectScreen(uiState = ConnectUiState.INITIAL, onAccountClick = onAccountClickMockk) } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/ConnectionStatusText.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/ConnectionStatusText.kt index 742302ce91ae..903bb412dc25 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/ConnectionStatusText.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/ConnectionStatusText.kt @@ -18,7 +18,7 @@ import net.mullvad.talpid.tunnel.ErrorStateCause private fun PreviewConnectionStatusText() { AppTheme { SpacedColumn { - ConnectionStatusText(TunnelState.Disconnected) + ConnectionStatusText(TunnelState.Disconnected()) ConnectionStatusText(TunnelState.Connecting(null, null)) ConnectionStatusText( state = TunnelState.Error(ErrorState(ErrorStateCause.Ipv6Unavailable, true)) 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 cbf1f53c3dc2..764626650724 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 @@ -19,7 +19,9 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -108,7 +110,6 @@ fun Connect(navigator: DestinationsNavigator) { onSwitchLocationClick = { navigator.navigate(SelectLocationDestination) { launchSingleTop = true } }, - onToggleTunnelInfo = connectViewModel::toggleTunnelInfoExpansion, onUpdateVersionClick = { val intent = Intent( @@ -135,7 +136,6 @@ fun ConnectScreen( onConnectClick: () -> Unit = {}, onCancelClick: () -> Unit = {}, onSwitchLocationClick: () -> Unit = {}, - onToggleTunnelInfo: () -> Unit = {}, onUpdateVersionClick: () -> Unit = {}, onManageAccountClick: () -> Unit = {}, onSettingsClick: () -> Unit = {}, @@ -230,12 +230,13 @@ fun ConnectScreen( color = MaterialTheme.colorScheme.onPrimary, modifier = Modifier.padding(horizontal = Dimens.sideMargin) ) + var expanded by rememberSaveable { mutableStateOf(false) } LocationInfo( - onToggleTunnelInfo = onToggleTunnelInfo, + onToggleTunnelInfo = { expanded = !expanded }, isVisible = - uiState.tunnelRealState != TunnelState.Disconnected && + uiState.tunnelRealState !is TunnelState.Disconnected && uiState.location?.hostname != null, - isExpanded = uiState.isTunnelInfoExpanded, + isExpanded = expanded, location = uiState.location, inAddress = uiState.inAddress, outAddress = uiState.outAddress, diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreen.kt index d9071be7d85e..d0d0c7460d8b 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreen.kt @@ -62,7 +62,7 @@ private fun PreviewOutOfTimeScreenDisconnected() { OutOfTimeScreen( uiState = OutOfTimeUiState( - tunnelState = TunnelState.Disconnected, + tunnelState = TunnelState.Disconnected(), "Heroic Frog", showSitePayment = true ), diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/ConnectUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/ConnectUiState.kt index 6ab4839bd159..2062092e17d0 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/ConnectUiState.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/ConnectUiState.kt @@ -15,7 +15,6 @@ data class ConnectUiState( val outAddress: String, val showLocation: Boolean, val inAppNotification: InAppNotification?, - val isTunnelInfoExpanded: Boolean, val deviceName: String?, val daysLeftUntilExpiry: Int? ) { @@ -24,12 +23,11 @@ data class ConnectUiState( ConnectUiState( location = null, relayLocation = null, - tunnelUiState = TunnelState.Disconnected, - tunnelRealState = TunnelState.Disconnected, + tunnelUiState = TunnelState.Disconnected(), + tunnelRealState = TunnelState.Disconnected(), inAddress = null, outAddress = "", showLocation = false, - isTunnelInfoExpanded = false, inAppNotification = null, deviceName = null, daysLeftUntilExpiry = null diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/OutOfTimeUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/OutOfTimeUiState.kt index 54fd414f866a..d72e01519494 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/OutOfTimeUiState.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/OutOfTimeUiState.kt @@ -3,7 +3,7 @@ package net.mullvad.mullvadvpn.compose.state import net.mullvad.mullvadvpn.model.TunnelState data class OutOfTimeUiState( - val tunnelState: TunnelState = TunnelState.Disconnected, + val tunnelState: TunnelState = TunnelState.Disconnected(), val deviceName: String = "", val showSitePayment: Boolean = false, val billingPaymentState: PaymentState? = null, diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/WelcomeUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/WelcomeUiState.kt index e2673a0ddf38..e43cf6bb9887 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/WelcomeUiState.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/WelcomeUiState.kt @@ -3,7 +3,7 @@ package net.mullvad.mullvadvpn.compose.state import net.mullvad.mullvadvpn.model.TunnelState data class WelcomeUiState( - val tunnelState: TunnelState = TunnelState.Disconnected, + val tunnelState: TunnelState = TunnelState.Disconnected(), val accountNumber: String? = null, val deviceName: String? = null, val showSitePayment: Boolean = false, diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ConnectionProxy.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ConnectionProxy.kt index d51bad461d9d..bbc267b2fac5 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ConnectionProxy.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ConnectionProxy.kt @@ -19,8 +19,8 @@ const val ANTICIPATED_STATE_TIMEOUT_MS = 1500L class ConnectionProxy(private val connection: Messenger, eventDispatcher: EventDispatcher) { private var resetAnticipatedStateJob: Job? = null - val onStateChange = EventNotifier(TunnelState.Disconnected) - val onUiStateChange = EventNotifier(TunnelState.Disconnected) + val onStateChange = EventNotifier(TunnelState.Disconnected()) + val onUiStateChange = EventNotifier(TunnelState.Disconnected()) var state by onStateChange.notifiable() private set diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/LocationInfoCache.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/LocationInfoCache.kt deleted file mode 100644 index 48f77d397dd9..000000000000 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/LocationInfoCache.kt +++ /dev/null @@ -1,26 +0,0 @@ -package net.mullvad.mullvadvpn.ui.serviceconnection - -import kotlin.properties.Delegates.observable -import net.mullvad.mullvadvpn.lib.ipc.Event -import net.mullvad.mullvadvpn.lib.ipc.EventDispatcher -import net.mullvad.mullvadvpn.model.GeoIpLocation - -class LocationInfoCache(eventDispatcher: EventDispatcher) { - private var location: GeoIpLocation? by - observable(null) { _, _, newLocation -> onNewLocation?.invoke(newLocation) } - - var onNewLocation by - observable<((GeoIpLocation?) -> Unit)?>(null) { _, _, callback -> - callback?.invoke(location) - } - - init { - eventDispatcher.registerHandler(Event.NewLocation::class) { event -> - location = event.location - } - } - - fun onDestroy() { - onNewLocation = null - } -} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnectionContainer.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnectionContainer.kt index ca156bed66e8..8aabe6c9f56c 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnectionContainer.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnectionContainer.kt @@ -28,7 +28,6 @@ class ServiceConnectionContainer( val authTokenCache = AuthTokenCache(connection, dispatcher) val connectionProxy = ConnectionProxy(connection, dispatcher) val deviceDataSource = ServiceConnectionDeviceDataSource(connection, dispatcher) - val locationInfoCache = LocationInfoCache(dispatcher) val settingsListener = SettingsListener(connection, dispatcher) val splitTunneling = SplitTunneling(connection, dispatcher) @@ -62,7 +61,6 @@ class ServiceConnectionContainer( authTokenCache.onDestroy() connectionProxy.onDestroy() - locationInfoCache.onDestroy() settingsListener.onDestroy() voucherRedeemer.onDestroy() diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/TunnelStateNotificationUseCase.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/TunnelStateNotificationUseCase.kt index f228bd7dbecc..dec794c86c8e 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/TunnelStateNotificationUseCase.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/TunnelStateNotificationUseCase.kt @@ -39,7 +39,7 @@ class TunnelStateNotificationUseCase( } is TunnelState.Error -> InAppNotification.TunnelStateError(tunnelUiState.errorState) is TunnelState.Connected, - TunnelState.Disconnected -> null + is TunnelState.Disconnected -> null } private fun ConnectionProxy.tunnelUiStateFlow(): Flow = 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 976eed1270e2..7a0ba055f423 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 @@ -5,30 +5,30 @@ import androidx.lifecycle.viewModelScope import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import net.mullvad.mullvadvpn.compose.state.ConnectUiState +import net.mullvad.mullvadvpn.model.GeoIpLocation import net.mullvad.mullvadvpn.model.TunnelState import net.mullvad.mullvadvpn.repository.AccountRepository import net.mullvad.mullvadvpn.repository.DeviceRepository import net.mullvad.mullvadvpn.repository.InAppNotificationController import net.mullvad.mullvadvpn.ui.serviceconnection.ConnectionProxy -import net.mullvad.mullvadvpn.ui.serviceconnection.LocationInfoCache import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionContainer import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionState @@ -70,48 +70,46 @@ class ConnectViewModel( } .shareIn(viewModelScope, SharingStarted.WhileSubscribed()) - private val _isTunnelInfoExpanded = MutableStateFlow(false) - val uiState: StateFlow = _shared .flatMapLatest { serviceConnection -> combine( - serviceConnection.locationInfoCache.locationCallbackFlow(), relayListUseCase.selectedRelayItem(), inAppNotificationController.notifications, serviceConnection.connectionProxy.tunnelUiStateFlow(), serviceConnection.connectionProxy.tunnelRealStateFlow(), + serviceConnection.connectionProxy.lastKnownLocation(), accountRepository.accountExpiryState, - _isTunnelInfoExpanded, deviceRepository.deviceState.map { it.deviceName() } ) { - location, relayLocation, notifications, tunnelUiState, tunnelRealState, + lastKnownDisconnectedLocation, accountExpiry, - isTunnelInfoExpanded, deviceName -> ConnectUiState( location = when (tunnelRealState) { - is TunnelState.Connected -> tunnelRealState.location + is TunnelState.Disconnected -> tunnelRealState.location() + ?: lastKnownDisconnectedLocation is TunnelState.Connecting -> tunnelRealState.location - else -> null - } - ?: location, + ?: relayLocation?.location?.location + is TunnelState.Connected -> tunnelRealState.location + is TunnelState.Disconnecting -> lastKnownDisconnectedLocation + is TunnelState.Error -> null + }, relayLocation = relayLocation, tunnelUiState = tunnelUiState, tunnelRealState = tunnelRealState, - isTunnelInfoExpanded = isTunnelInfoExpanded, inAddress = when (tunnelRealState) { is TunnelState.Connected -> tunnelRealState.endpoint.toInAddress() is TunnelState.Connecting -> tunnelRealState.endpoint?.toInAddress() else -> null }, - outAddress = location?.toOutAddress() ?: "", + outAddress = tunnelRealState.location()?.toOutAddress() ?: "", showLocation = when (tunnelUiState) { is TunnelState.Disconnected -> true @@ -147,20 +145,18 @@ class ConnectViewModel( } } - private fun LocationInfoCache.locationCallbackFlow() = callbackFlow { - onNewLocation = { this.trySend(it) } - awaitClose { onNewLocation = null } - } - private fun ConnectionProxy.tunnelUiStateFlow(): Flow = callbackFlowFromNotifier(this.onUiStateChange) private fun ConnectionProxy.tunnelRealStateFlow(): Flow = callbackFlowFromNotifier(this.onStateChange) - fun toggleTunnelInfoExpansion() { - _isTunnelInfoExpanded.value = _isTunnelInfoExpanded.value.not() - } + private fun ConnectionProxy.lastKnownLocation(): Flow = + tunnelRealStateFlow() + .filterIsInstance() + .filter { it.location == null } + .map { it.location } + .onStart { emit(null) } fun onDisconnectClick() { serviceConnectionManager.connectionProxy()?.disconnect() diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/OutOfTimeUseCaseTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/OutOfTimeUseCaseTest.kt index 74683813aede..bdc5ea49b859 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/OutOfTimeUseCaseTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/OutOfTimeUseCaseTest.kt @@ -63,7 +63,7 @@ class OutOfTimeUseCaseTest { val expiredAccountExpiry = AccountExpiry.Available(DateTime.now().plusDays(1)) val tunnelStateChanges = listOf( - TunnelState.Disconnected, + TunnelState.Disconnected(), TunnelState.Connected(mockk(), null), TunnelState.Connecting(null, null), TunnelState.Disconnecting(mockk()), diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/TunnelStateNotificationUseCaseTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/TunnelStateNotificationUseCaseTest.kt index 1b89c92be732..e67630642dc9 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/TunnelStateNotificationUseCaseTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/TunnelStateNotificationUseCaseTest.kt @@ -35,7 +35,7 @@ class TunnelStateNotificationUseCaseTest { MutableStateFlow(ServiceConnectionState.Disconnected) private lateinit var tunnelStateNotificationUseCase: TunnelStateNotificationUseCase - private val eventNotifierTunnelUiState = EventNotifier(TunnelState.Disconnected) + private val eventNotifierTunnelUiState = EventNotifier(TunnelState.Disconnected()) @Before fun setup() { 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 35898df4abbc..7361264bb76c 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 @@ -6,12 +6,11 @@ import io.mockk.coEvery import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic -import io.mockk.slot import io.mockk.unmockkAll import io.mockk.verify import kotlin.test.assertEquals import kotlin.test.assertIs -import kotlin.test.assertTrue +import kotlin.test.assertNull import kotlinx.coroutines.async import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.MutableStateFlow @@ -33,7 +32,6 @@ import net.mullvad.mullvadvpn.ui.VersionInfo import net.mullvad.mullvadvpn.ui.serviceconnection.AppVersionInfoCache import net.mullvad.mullvadvpn.ui.serviceconnection.AuthTokenCache import net.mullvad.mullvadvpn.ui.serviceconnection.ConnectionProxy -import net.mullvad.mullvadvpn.ui.serviceconnection.LocationInfoCache import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionContainer import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionState @@ -73,7 +71,6 @@ class ConnectViewModelTest { // Service connections private val mockServiceConnectionContainer: ServiceConnectionContainer = mockk() - private val mockLocationInfoCache: LocationInfoCache = mockk(relaxUnitFun = true) private lateinit var mockAppVersionInfoCache: AppVersionInfoCache private val mockConnectionProxy: ConnectionProxy = mockk() private val mockLocation: GeoIpLocation = mockk(relaxed = true) @@ -93,12 +90,10 @@ class ConnectViewModelTest { // Payment use case private val mockPaymentUseCase: PaymentUseCase = mockk(relaxed = true) - // Captures - private val locationSlot = slot<((GeoIpLocation?) -> Unit)>() - // Event notifiers - private val eventNotifierTunnelUiState = EventNotifier(TunnelState.Disconnected) - private val eventNotifierTunnelRealState = EventNotifier(TunnelState.Disconnected) + private val eventNotifierTunnelUiState = EventNotifier(TunnelState.Disconnected()) + private val eventNotifierTunnelRealState = + EventNotifier(TunnelState.Disconnected()) // Flows private val selectedRelayFlow = MutableStateFlow(null) @@ -118,7 +113,6 @@ class ConnectViewModelTest { } every { mockServiceConnectionManager.connectionState } returns serviceConnectionState - every { mockServiceConnectionContainer.locationInfoCache } returns mockLocationInfoCache every { mockServiceConnectionContainer.appVersionInfoCache } returns mockAppVersionInfoCache every { mockServiceConnectionContainer.connectionProxy } returns mockConnectionProxy @@ -134,7 +128,6 @@ class ConnectViewModelTest { every { mockLocation.country } returns "dummy country" // Listeners - every { mockLocationInfoCache.onNewLocation = capture(locationSlot) } answers {} every { mockAppVersionInfoCache.onUpdate = any() } answers {} // Flows @@ -165,30 +158,15 @@ class ConnectViewModelTest { viewModel.uiState.test { assertEquals(ConnectUiState.INITIAL, awaitItem()) } } - @Test - fun testTunnelInfoExpandedUpdate() = - runTest(testCoroutineRule.testDispatcher) { - viewModel.uiState.test { - assertEquals(ConnectUiState.INITIAL, awaitItem()) - serviceConnectionState.value = - ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer) - locationSlot.captured.invoke(mockLocation) - viewModel.toggleTunnelInfoExpansion() - val result = awaitItem() - assertTrue(result.isTunnelInfoExpanded) - } - } - @Test fun testTunnelRealStateUpdate() = runTest(testCoroutineRule.testDispatcher) { - val tunnelRealStateTestItem = TunnelState.Connected(mockk(relaxed = true), mockk()) + val tunnelRealStateTestItem = TunnelState.Connected(mockk(relaxed = true), null) viewModel.uiState.test { assertEquals(ConnectUiState.INITIAL, awaitItem()) serviceConnectionState.value = ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer) - locationSlot.captured.invoke(mockLocation) eventNotifierTunnelRealState.notify(tunnelRealStateTestItem) val result = awaitItem() assertEquals(tunnelRealStateTestItem, result.tunnelRealState) @@ -204,7 +182,6 @@ class ConnectViewModelTest { assertEquals(ConnectUiState.INITIAL, awaitItem()) serviceConnectionState.value = ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer) - locationSlot.captured.invoke(mockLocation) eventNotifierTunnelUiState.notify(tunnelUiStateTestItem) val result = awaitItem() assertEquals(tunnelUiStateTestItem, result.tunnelUiState) @@ -222,7 +199,6 @@ class ConnectViewModelTest { assertEquals(ConnectUiState.INITIAL, awaitItem()) serviceConnectionState.value = ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer) - locationSlot.captured.invoke(mockLocation) val result = awaitItem() assertEquals(relayTestItem, result.relayLocation) } @@ -240,13 +216,19 @@ class ConnectViewModelTest { hostname = "Host" ) + // Act, Assert viewModel.uiState.test { assertEquals(ConnectUiState.INITIAL, awaitItem()) + eventNotifierTunnelRealState.notify(TunnelState.Disconnected(null)) + serviceConnectionState.value = ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer) - locationSlot.captured.invoke(locationTestItem) - val result = awaitItem() - assertEquals(locationTestItem, result.location) + // Start of with no location + assertNull(awaitItem().location) + + // After updated we show latest + eventNotifierTunnelRealState.notify(TunnelState.Disconnected(locationTestItem)) + assertEquals(locationTestItem, awaitItem().location) } } @@ -261,7 +243,6 @@ class ConnectViewModelTest { assertEquals(ConnectUiState.INITIAL, awaitItem()) serviceConnectionState.value = ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer) - locationSlot.captured.invoke(locationTestItem) expectNoEvents() val result = awaitItem() assertEquals(locationTestItem, result.location) @@ -319,7 +300,6 @@ class ConnectViewModelTest { assertEquals(ConnectUiState.INITIAL, awaitItem()) serviceConnectionState.value = ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer) - locationSlot.captured.invoke(mockLocation) eventNotifierTunnelUiState.notify(tunnelUiState) val result = awaitItem() assertEquals(expectedConnectNotificationState, result.inAppNotification) @@ -355,7 +335,6 @@ class ConnectViewModelTest { awaitItem() serviceConnectionState.value = ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer) - locationSlot.captured.invoke(mockLocation) outOfTimeViewFlow.value = true awaitItem() } diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceRevokedViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceRevokedViewModelTest.kt index 8959b1f9e375..a73ecfc4e786 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceRevokedViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceRevokedViewModelTest.kt @@ -105,7 +105,7 @@ class DeviceRevokedViewModelTest { // Arrange val mockedContainer = mockk().also { - every { it.connectionProxy.state } returns TunnelState.Disconnected + every { it.connectionProxy.state } returns TunnelState.Disconnected() every { it.connectionProxy.disconnect() } just Runs every { mockedAccountRepository.logout() } just Runs } diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModelTest.kt index 0232f12e89b3..93f53707f5ac 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModelTest.kt @@ -59,7 +59,8 @@ class OutOfTimeViewModelTest { private val mockConnectionProxy: ConnectionProxy = mockk() // Event notifiers - private val eventNotifierTunnelRealState = EventNotifier(TunnelState.Disconnected) + private val eventNotifierTunnelRealState = + EventNotifier(TunnelState.Disconnected()) private val mockAccountRepository: AccountRepository = mockk(relaxed = true) private val mockDeviceRepository: DeviceRepository = mockk() diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModelTest.kt index 433a9f570903..d999d80df9a2 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModelTest.kt @@ -58,7 +58,7 @@ class WelcomeViewModelTest { private val mockConnectionProxy: ConnectionProxy = mockk() // Event notifiers - private val eventNotifierTunnelUiState = EventNotifier(TunnelState.Disconnected) + private val eventNotifierTunnelUiState = EventNotifier(TunnelState.Disconnected()) private val mockAccountRepository: AccountRepository = mockk(relaxed = true) private val mockDeviceRepository: DeviceRepository = mockk() diff --git a/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/Event.kt b/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/Event.kt index e1079807f169..69c28bb379da 100644 --- a/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/Event.kt +++ b/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/Event.kt @@ -7,7 +7,6 @@ import net.mullvad.mullvadvpn.model.AccountExpiry import net.mullvad.mullvadvpn.model.AccountHistory import net.mullvad.mullvadvpn.model.DeviceListEvent import net.mullvad.mullvadvpn.model.DeviceState -import net.mullvad.mullvadvpn.model.GeoIpLocation import net.mullvad.mullvadvpn.model.LoginResult import net.mullvad.mullvadvpn.model.PlayPurchaseInitResult import net.mullvad.mullvadvpn.model.PlayPurchaseVerifyResult @@ -45,8 +44,6 @@ sealed class Event : Message.EventMessage() { @Parcelize data class LoginEvent(val result: LoginResult) : Event() - @Parcelize data class NewLocation(val location: GeoIpLocation?) : Event() - @Parcelize data class NewRelayList(val relayList: RelayList?) : Event() @Parcelize data class SettingsUpdate(val settings: Settings?) : Event() diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/TunnelState.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/TunnelState.kt index 26f776b2236b..4ab925d01427 100644 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/TunnelState.kt +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/TunnelState.kt @@ -5,11 +5,10 @@ import kotlinx.parcelize.Parcelize import net.mullvad.talpid.net.TunnelEndpoint import net.mullvad.talpid.tunnel.ActionAfterDisconnect import net.mullvad.talpid.tunnel.ErrorState -import net.mullvad.talpid.tunnel.ErrorStateCause -import net.mullvad.talpid.tunnel.FirewallPolicyError sealed class TunnelState : Parcelable { - @Parcelize object Disconnected : TunnelState(), Parcelable + @Parcelize + data class Disconnected(val location: GeoIpLocation? = null) : TunnelState(), Parcelable @Parcelize class Connecting(val endpoint: TunnelEndpoint?, val location: GeoIpLocation?) : @@ -25,6 +24,16 @@ sealed class TunnelState : Parcelable { @Parcelize class Error(val errorState: ErrorState) : TunnelState(), Parcelable + fun location(): GeoIpLocation? { + return when (this) { + is Connected -> location + is Connecting -> location + is Disconnecting -> null + is Disconnected -> location + is Error -> null + } + } + fun isSecured(): Boolean { return when (this) { is Connected, @@ -34,61 +43,4 @@ sealed class TunnelState : Parcelable { is Error -> this.errorState.isBlocking } } - - override fun toString(): String = - when (this) { - is Disconnected -> DISCONNECTED - is Connecting -> CONNECTING - is Connected -> CONNECTED - is Disconnecting -> { - if (actionAfterDisconnect == ActionAfterDisconnect.Reconnect) { - RECONNECTING - } else { - DISCONNECTING - } - } - is Error -> { - if (errorState.isBlocking) { - BLOCKING - } else { - ERROR - } - } - } - - companion object { - const val DISCONNECTED = "disconnected" - const val CONNECTING = "connecting" - const val CONNECTED = "connected" - const val RECONNECTING = "reconnecting" - const val DISCONNECTING = "disconnecting" - const val BLOCKING = "blocking" - const val ERROR = "error" - - fun fromString(description: String, endpoint: TunnelEndpoint?): TunnelState { - return when (description) { - DISCONNECTED -> Disconnected - CONNECTING -> Connecting(endpoint, null) - CONNECTED -> Connected(endpoint!!, null) - RECONNECTING -> Disconnecting(ActionAfterDisconnect.Reconnect) - DISCONNECTING -> Disconnecting(ActionAfterDisconnect.Nothing) - BLOCKING -> Error(ErrorState(ErrorStateCause.StartTunnelError, true)) - ERROR -> { - Error( - ErrorState( - ErrorStateCause.SetFirewallPolicyError(FirewallPolicyError.Generic), - false - ) - ) - } - else -> - Error( - ErrorState( - ErrorStateCause.SetFirewallPolicyError(FirewallPolicyError.Generic), - false - ) - ) - } - } - } } diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadDaemon.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadDaemon.kt index 0cfd0e1f7db3..107f5d35fe2f 100644 --- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadDaemon.kt +++ b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadDaemon.kt @@ -10,7 +10,6 @@ import net.mullvad.mullvadvpn.model.DeviceEvent import net.mullvad.mullvadvpn.model.DeviceListEvent import net.mullvad.mullvadvpn.model.DeviceState import net.mullvad.mullvadvpn.model.DnsOptions -import net.mullvad.mullvadvpn.model.GeoIpLocation import net.mullvad.mullvadvpn.model.GetAccountDataResult import net.mullvad.mullvadvpn.model.LoginResult import net.mullvad.mullvadvpn.model.ObfuscationSettings @@ -34,7 +33,7 @@ class MullvadDaemon( protected var daemonInterfaceAddress = 0L val onSettingsChange = EventNotifier(null) - var onTunnelStateChange = EventNotifier(TunnelState.Disconnected) + var onTunnelStateChange = EventNotifier(TunnelState.Disconnected()) var onAppVersionInfoChange: ((AppVersionInfo) -> Unit)? = null var onRelayListChange: ((RelayList) -> Unit)? = null @@ -58,7 +57,7 @@ class MullvadDaemon( onSettingsChange.notify(getSettings()) - onTunnelStateChange.notify(getState() ?: TunnelState.Disconnected) + onTunnelStateChange.notify(getState() ?: TunnelState.Disconnected()) } fun connect() { @@ -85,10 +84,6 @@ class MullvadDaemon( return getWwwAuthToken(daemonInterfaceAddress) ?: "" } - fun getCurrentLocation(): GeoIpLocation? { - return getCurrentLocation(daemonInterfaceAddress) - } - fun getCurrentVersion(): String? { return getCurrentVersion(daemonInterfaceAddress) } @@ -229,8 +224,6 @@ class MullvadDaemon( private external fun getWwwAuthToken(daemonInterfaceAddress: Long): String? - private external fun getCurrentLocation(daemonInterfaceAddress: Long): GeoIpLocation? - private external fun getCurrentVersion(daemonInterfaceAddress: Long): String? private external fun getRelayLocations(daemonInterfaceAddress: Long): RelayList? diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadVpnService.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadVpnService.kt index e714295d9c46..b0c55540a672 100644 --- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadVpnService.kt +++ b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadVpnService.kt @@ -189,7 +189,7 @@ class MullvadVpnService : TalpidVpnService() { override fun onTaskRemoved(rootIntent: Intent?) { connectionProxy.onStateChange.latestEvent.let { tunnelState -> Log.d(TAG, "Task removed (tunnelState=$tunnelState)") - if (tunnelState == TunnelState.Disconnected) { + if (tunnelState is TunnelState.Disconnected) { notificationManager.cancelNotification() stop() } diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/ConnectionProxy.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/ConnectionProxy.kt index a2c97a05bd1a..65a27c8f695b 100644 --- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/ConnectionProxy.kt +++ b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/ConnectionProxy.kt @@ -20,7 +20,7 @@ class ConnectionProxy(val vpnPermission: VpnPermission, endpoint: ServiceEndpoin private val commandChannel = spawnActor() private val daemon = endpoint.intermittentDaemon - private val initialState = TunnelState.Disconnected + private val initialState = TunnelState.Disconnected() var onStateChange = EventNotifier(initialState) diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/LocationInfoCache.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/LocationInfoCache.kt deleted file mode 100644 index 2d06cc109f73..000000000000 --- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/LocationInfoCache.kt +++ /dev/null @@ -1,139 +0,0 @@ -package net.mullvad.mullvadvpn.service.endpoint - -import kotlin.properties.Delegates.observable -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.channels.ClosedReceiveChannelException -import kotlinx.coroutines.channels.ReceiveChannel -import kotlinx.coroutines.channels.actor -import kotlinx.coroutines.channels.trySendBlocking -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.receiveAsFlow -import net.mullvad.mullvadvpn.lib.common.util.toGeographicLocationConstraint -import net.mullvad.mullvadvpn.lib.ipc.Event -import net.mullvad.mullvadvpn.model.Constraint -import net.mullvad.mullvadvpn.model.GeoIpLocation -import net.mullvad.mullvadvpn.model.RelaySettings -import net.mullvad.mullvadvpn.model.TunnelState -import net.mullvad.mullvadvpn.service.util.ExponentialBackoff -import net.mullvad.talpid.tunnel.ActionAfterDisconnect - -class LocationInfoCache(private val endpoint: ServiceEndpoint) { - - private val fetchRetryDelays = - ExponentialBackoff().apply { - scale = 50 - cap = 30 /* min */ * 60 /* s */ * 1000 /* ms */ - count = 17 // ceil(log2(cap / scale) + 1) - } - - private val fetchRequestChannel = runFetcher() - - private val daemon - get() = endpoint.intermittentDaemon - - private var lastKnownRealLocation: GeoIpLocation? = null - private var selectedRelayLocation: GeoIpLocation? = null - - var location: GeoIpLocation? by - observable(null) { _, _, newLocation -> endpoint.sendEvent(Event.NewLocation(newLocation)) } - - var state by - observable(TunnelState.Disconnected) { _, _, newState -> - when (newState) { - is TunnelState.Disconnected -> { - location = lastKnownRealLocation - fetchRequestChannel.trySendBlocking(RequestFetch.ForRealLocation) - } - is TunnelState.Connecting -> location = newState.location - is TunnelState.Connected -> { - location = newState.location - fetchRequestChannel.trySendBlocking(RequestFetch.ForRelayLocation) - } - is TunnelState.Disconnecting -> { - when (newState.actionAfterDisconnect) { - ActionAfterDisconnect.Nothing -> location = lastKnownRealLocation - ActionAfterDisconnect.Block -> location = null - ActionAfterDisconnect.Reconnect -> { - lastKnownRealLocation?.let { location = it } - } - } - } - is TunnelState.Error -> location = null - } - } - - init { - endpoint.connectionProxy.onStateChange.subscribe(this) { newState -> state = newState } - - endpoint.connectivityListener.connectivityNotifier.subscribe(this) { isConnected -> - if (isConnected && state is TunnelState.Disconnected) { - fetchRequestChannel.trySendBlocking(RequestFetch.ForRealLocation) - } - } - - endpoint.settingsListener.relaySettingsNotifier.subscribe(this, ::updateSelectedLocation) - } - - fun onDestroy() { - endpoint.connectionProxy.onStateChange.unsubscribe(this) - endpoint.connectivityListener.connectivityNotifier.unsubscribe(this) - endpoint.settingsListener.relaySettingsNotifier.unsubscribe(this) - - fetchRequestChannel.close() - } - - private fun runFetcher() = - GlobalScope.actor(Dispatchers.Default, Channel.CONFLATED) { - try { - fetcherLoop(channel) - } catch (exception: ClosedReceiveChannelException) {} - } - - private suspend fun fetcherLoop(channel: ReceiveChannel) { - channel - .receiveAsFlow() - .flatMapLatest(::fetchCurrentLocation) - .collect(::handleFetchedLocation) - } - - private fun fetchCurrentLocation(fetchType: RequestFetch) = flow { - var newLocation = daemon.await().getCurrentLocation() - - fetchRetryDelays.reset() - - while (newLocation == null) { - delay(fetchRetryDelays.next()) - newLocation = daemon.await().getCurrentLocation() - } - - emit(Pair(newLocation, fetchType)) - } - - private suspend fun handleFetchedLocation(pairItem: Pair) { - val (newLocation, fetchType) = pairItem - - if (fetchType == RequestFetch.ForRealLocation) { - lastKnownRealLocation = newLocation - } - - location = newLocation - } - - private fun updateSelectedLocation(relaySettings: RelaySettings?) { - val settings = relaySettings as? RelaySettings.Normal - val constraint = settings?.relayConstraints?.location as? Constraint.Only - - selectedRelayLocation = constraint?.value?.toGeographicLocationConstraint()?.location - } - - companion object { - private enum class RequestFetch { - ForRealLocation, - ForRelayLocation, - } - } -} diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/ServiceEndpoint.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/ServiceEndpoint.kt index 70e7807ff921..09fc73e5d3cd 100644 --- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/ServiceEndpoint.kt +++ b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/ServiceEndpoint.kt @@ -46,7 +46,6 @@ class ServiceEndpoint( val appVersionInfoCache = AppVersionInfoCache(this) val authTokenCache = AuthTokenCache(this) val customDns = CustomDns(this) - val locationInfoCache = LocationInfoCache(this) val relayListListener = RelayListListener(this) val splitTunneling = SplitTunneling(SplitTunnelingPersistence(context), this) val voucherRedeemer = VoucherRedeemer(this, accountCache) @@ -77,7 +76,6 @@ class ServiceEndpoint( connectionProxy.onDestroy() customDns.onDestroy() deviceRepositoryBackend.onDestroy() - locationInfoCache.onDestroy() relayListListener.onDestroy() settingsListener.onDestroy() splitTunneling.onDestroy() @@ -127,7 +125,6 @@ class ServiceEndpoint( Event.TunnelStateChange(connectionProxy.state), Event.AccountHistoryEvent(accountCache.onAccountHistoryChange.latestEvent), Event.SettingsUpdate(settingsListener.settings), - Event.NewLocation(locationInfoCache.location), Event.SplitTunnelingUpdate(splitTunneling.onChange.latestEvent), Event.CurrentVersion(appVersionInfoCache.currentVersion), Event.AppVersionInfo(appVersionInfoCache.appVersionInfo), diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/TunnelStateNotification.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/TunnelStateNotification.kt index 97bddc860805..ede083a2a182 100644 --- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/TunnelStateNotification.kt +++ b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/TunnelStateNotification.kt @@ -77,7 +77,7 @@ class TunnelStateNotification(val context: Context) { var showAction by observable(false) { _, _, _ -> update() } var tunnelState by - observable(TunnelState.Disconnected) { _, _, newState -> + observable(TunnelState.Disconnected()) { _, _, newState -> val isReconnecting = newState is TunnelState.Connecting && reconnecting val shouldBeginReconnecting = (newState as? TunnelState.Disconnecting)?.actionAfterDisconnect == diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/util/ExponentialBackoff.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/util/ExponentialBackoff.kt deleted file mode 100644 index 12e94a92411b..000000000000 --- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/util/ExponentialBackoff.kt +++ /dev/null @@ -1,52 +0,0 @@ -package net.mullvad.mullvadvpn.service.util - -// Calculates a series of delays that increase exponentially. -// -// The delays follow the formula: -// -// (base ^ retryAttempt) * scale -// -// but it is never larger than the specified cap value. -class ExponentialBackoff : Iterator { - private var unscaledValue = 1L - private var current = 1L - - var iteration = 1 - private set - - var base = 2L - var scale = 1000L - var cap = Long.MAX_VALUE - var count: Int? = null - - override fun hasNext(): Boolean { - val maxIterations = count - - if (maxIterations != null) { - return iteration < maxIterations - } else { - return true - } - } - - override fun next(): Long { - iteration += 1 - - if (current >= cap) { - return cap - } else { - val value = current - - unscaledValue *= base - current = Math.min(cap, scale * unscaledValue) - - return value - } - } - - fun reset() { - unscaledValue = 1L - current = 1L - iteration = 1 - } -} diff --git a/android/tile/src/main/kotlin/net/mullvad/mullvadvpn/tile/ServiceConnection.kt b/android/tile/src/main/kotlin/net/mullvad/mullvadvpn/tile/ServiceConnection.kt index a26351162d02..93218c66dcdf 100644 --- a/android/tile/src/main/kotlin/net/mullvad/mullvadvpn/tile/ServiceConnection.kt +++ b/android/tile/src/main/kotlin/net/mullvad/mullvadvpn/tile/ServiceConnection.kt @@ -60,7 +60,7 @@ class ServiceConnection(context: Context, scope: CoroutineScope) { subscribeToState( Event.TunnelStateChange::class, scope, - TunnelState.Disconnected + TunnelState.Disconnected() ) { tunnelState } diff --git a/mullvad-jni/src/daemon_interface.rs b/mullvad-jni/src/daemon_interface.rs index ff00f4a825b8..feea551557dd 100644 --- a/mullvad-jni/src/daemon_interface.rs +++ b/mullvad-jni/src/daemon_interface.rs @@ -114,14 +114,6 @@ impl DaemonInterface { .map_err(Error::from) } - pub fn get_current_location(&self) -> Result> { - let (tx, rx) = oneshot::channel(); - - self.send_command(DaemonCommand::GetCurrentLocation(tx))?; - - block_on(rx).map_err(|_| Error::NoResponse) - } - pub fn get_current_version(&self) -> Result { let (tx, rx) = oneshot::channel(); diff --git a/mullvad-jni/src/lib.rs b/mullvad-jni/src/lib.rs index a2c35864315b..8c5115789665 100644 --- a/mullvad-jni/src/lib.rs +++ b/mullvad-jni/src/lib.rs @@ -760,34 +760,6 @@ pub extern "system" fn Java_net_mullvad_mullvadvpn_service_MullvadDaemon_getWwwA } } -#[no_mangle] -#[allow(non_snake_case)] -pub extern "system" fn Java_net_mullvad_mullvadvpn_service_MullvadDaemon_getCurrentLocation< - 'env, ->( - env: JNIEnv<'env>, - _: JObject<'_>, - daemon_interface_address: jlong, -) -> JObject<'env> { - let env = JnixEnv::from(env); - - // SAFETY: The address points to an instance valid for the duration of this function call - if let Some(daemon_interface) = unsafe { get_daemon_interface(daemon_interface_address) } { - match daemon_interface.get_current_location() { - Ok(location) => location.into_java(&env).forget(), - Err(error) => { - log::error!( - "{}", - error.display_chain_with_msg("Failed to get current location") - ); - JObject::null() - } - } - } else { - JObject::null() - } -} - #[no_mangle] #[allow(non_snake_case)] pub extern "system" fn Java_net_mullvad_mullvadvpn_service_MullvadDaemon_getCurrentVersion<'env>(