diff options
Diffstat (limited to 'android')
13 files changed, 217 insertions, 190 deletions
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Event.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Event.kt index d9df17e9c7..ba94206a73 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Event.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Event.kt @@ -3,6 +3,7 @@ package net.mullvad.mullvadvpn.ipc import android.os.Message as RawMessage import android.os.Parcelable import kotlinx.parcelize.Parcelize +import net.mullvad.mullvadvpn.model.GeoIpLocation import net.mullvad.mullvadvpn.model.Settings // Events that can be sent from the service @@ -14,6 +15,9 @@ sealed class Event : Message(), Parcelable { object ListenerReady : Event(), Parcelable @Parcelize + data class NewLocation(val location: GeoIpLocation?) : Event(), Parcelable + + @Parcelize data class SettingsUpdate(val settings: Settings?) : Event(), Parcelable companion object { diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Request.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Request.kt index 47b613816b..388f60b07e 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Request.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Request.kt @@ -11,7 +11,7 @@ sealed class Request : Message(), Parcelable { protected override val messageKey = MESSAGE_KEY @Parcelize - class RegisterListener(val listener: Messenger) : Request(), Parcelable + data class RegisterListener(val listener: Messenger) : Request(), Parcelable companion object { private const val MESSAGE_KEY = "request" diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/model/GeoIpLocation.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/model/GeoIpLocation.kt index 1f4acef73f..e15ab20376 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/model/GeoIpLocation.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/model/GeoIpLocation.kt @@ -1,11 +1,14 @@ package net.mullvad.mullvadvpn.model +import android.os.Parcelable import java.net.InetAddress +import kotlinx.parcelize.Parcelize +@Parcelize data class GeoIpLocation( val ipv4: InetAddress?, val ipv6: InetAddress?, val country: String, val city: String?, val hostname: String? -) +) : Parcelable diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/model/LocationConstraint.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/model/LocationConstraint.kt index 998167468d..55757e5b8f 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/model/LocationConstraint.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/model/LocationConstraint.kt @@ -3,16 +3,31 @@ package net.mullvad.mullvadvpn.model import android.os.Parcelable import kotlinx.parcelize.Parcelize -sealed class LocationConstraint(val code: Array<String>) : Parcelable { +sealed class LocationConstraint : Parcelable { + abstract val location: GeoIpLocation + @Parcelize - data class Country(val countryCode: String) : - LocationConstraint(arrayOf(countryCode)), Parcelable + data class Country(val countryCode: String) : LocationConstraint(), Parcelable { + override val location: GeoIpLocation + get() = GeoIpLocation(null, null, countryCode, null, null) + } @Parcelize - data class City(val countryCode: String, val cityCode: String) : - LocationConstraint(arrayOf(countryCode, cityCode)), Parcelable + data class City( + val countryCode: String, + val cityCode: String + ) : LocationConstraint(), Parcelable { + override val location: GeoIpLocation + get() = GeoIpLocation(null, null, countryCode, cityCode, null) + } @Parcelize - data class Hostname(val countryCode: String, val cityCode: String, val hostname: String) : - LocationConstraint(arrayOf(countryCode, cityCode, hostname)), Parcelable + data class Hostname( + val countryCode: String, + val cityCode: String, + val hostname: String + ) : LocationConstraint(), Parcelable { + override val location: GeoIpLocation + get() = GeoIpLocation(null, null, countryCode, cityCode, hostname) + } } diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/LocationInfoCache.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/LocationInfoCache.kt deleted file mode 100644 index bd45e78ce5..0000000000 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/LocationInfoCache.kt +++ /dev/null @@ -1,171 +0,0 @@ -package net.mullvad.mullvadvpn.service - -import kotlin.properties.Delegates.observable -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.TimeoutCancellationException -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.channels.ClosedReceiveChannelException -import kotlinx.coroutines.channels.ReceiveChannel -import kotlinx.coroutines.channels.actor -import kotlinx.coroutines.channels.sendBlocking -import kotlinx.coroutines.withTimeout -import net.mullvad.mullvadvpn.model.GeoIpLocation -import net.mullvad.mullvadvpn.model.TunnelState -import net.mullvad.mullvadvpn.relaylist.Relay -import net.mullvad.mullvadvpn.relaylist.RelayCity -import net.mullvad.mullvadvpn.relaylist.RelayCountry -import net.mullvad.mullvadvpn.relaylist.RelayItem -import net.mullvad.mullvadvpn.util.ExponentialBackoff -import net.mullvad.talpid.ConnectivityListener -import net.mullvad.talpid.tunnel.ActionAfterDisconnect - -class LocationInfoCache( - val daemon: MullvadDaemon, - val connectionProxy: ConnectionProxy, - val connectivityListener: ConnectivityListener -) { - companion object { - private enum class RequestFetch { - ForRealLocation, - ForRelayLocation, - } - } - - private val fetchRequestChannel = runFetcher() - - private var lastKnownRealLocation: GeoIpLocation? = null - private var selectedRelayLocation: GeoIpLocation? = null - - var onNewLocation by observable<((GeoIpLocation?) -> Unit)?>(null) { _, _, callback -> - callback?.invoke(location) - } - - var location: GeoIpLocation? by observable(null) { _, _, newLocation -> - onNewLocation?.invoke(newLocation) - } - - var state by observable<TunnelState>(TunnelState.Disconnected) { _, _, newState -> - when (newState) { - is TunnelState.Disconnected -> { - location = lastKnownRealLocation - fetchRequestChannel.sendBlocking(RequestFetch.ForRealLocation) - } - is TunnelState.Connecting -> location = newState.location - is TunnelState.Connected -> { - location = newState.location - fetchRequestChannel.sendBlocking(RequestFetch.ForRelayLocation) - } - is TunnelState.Disconnecting -> { - when (newState.actionAfterDisconnect) { - ActionAfterDisconnect.Nothing -> location = lastKnownRealLocation - ActionAfterDisconnect.Block -> location = null - ActionAfterDisconnect.Reconnect -> location = selectedRelayLocation - } - } - is TunnelState.Error -> location = null - } - } - - var selectedRelay by observable<RelayItem?>(null) { _, oldRelay, newRelay -> - if (newRelay != oldRelay) { - updateSelectedRelayLocation(newRelay) - } - } - - init { - connectivityListener.connectivityNotifier.subscribe(this) { isConnected -> - if (isConnected && state is TunnelState.Disconnected) { - fetchRequestChannel.sendBlocking(RequestFetch.ForRealLocation) - } - } - - connectionProxy.onStateChange.subscribe(this) { realState -> - state = realState - } - } - - fun onDestroy() { - connectivityListener.connectivityNotifier.unsubscribe(this) - connectionProxy.onStateChange.unsubscribe(this) - fetchRequestChannel.close() - } - - private fun updateSelectedRelayLocation(relayItem: RelayItem?) { - selectedRelayLocation = when (relayItem) { - is RelayCountry -> GeoIpLocation(null, null, relayItem.name, null, null) - is RelayCity -> GeoIpLocation( - null, - null, - relayItem.country.name, - relayItem.name, - null - ) - is Relay -> GeoIpLocation( - null, - null, - relayItem.city.country.name, - relayItem.city.name, - relayItem.name - ) - else -> null - } - } - - private fun runFetcher() = GlobalScope.actor<RequestFetch>( - Dispatchers.Default, - Channel.CONFLATED - ) { - try { - fetcherLoop(channel) - } catch (exception: ClosedReceiveChannelException) { - } - } - - private suspend fun fetcherLoop(channel: ReceiveChannel<RequestFetch>) { - val delays = ExponentialBackoff().apply { - scale = 50 - cap = 30 /* min */ * 60 /* s */ * 1000 /* ms */ - count = 17 // ceil(log2(cap / scale) + 1) - } - - while (true) { - var fetchType = channel.receive() - var newLocation = daemon.getCurrentLocation() - - while (newLocation == null || !channel.isEmpty) { - fetchType = delayOrReceive(delays, channel, fetchType) - newLocation = daemon.getCurrentLocation() - } - - handleNewLocation(newLocation, fetchType) - delays.reset() - } - } - - private suspend fun delayOrReceive( - delays: ExponentialBackoff, - channel: ReceiveChannel<RequestFetch>, - currentValue: RequestFetch - ): RequestFetch { - try { - val newValue = withTimeout(delays.next()) { - channel.receive() - } - - delays.reset() - - return newValue - } catch (timeOut: TimeoutCancellationException) { - return currentValue - } - } - - private fun handleNewLocation(newLocation: GeoIpLocation, fetchType: RequestFetch) { - if (fetchType == RequestFetch.ForRealLocation) { - lastKnownRealLocation = newLocation - } - - location = newLocation - } -} diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadVpnService.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadVpnService.kt index 46719f9806..91bfc8118b 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadVpnService.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadVpnService.kt @@ -105,7 +105,11 @@ class MullvadVpnService : TalpidVpnService() { notificationManager = ForegroundNotificationManager(this, serviceNotifier, keyguardManager) tunnelStateUpdater = TunnelStateUpdater(this, serviceNotifier) - endpoint = ServiceEndpoint(Looper.getMainLooper(), daemonInstance.intermittentDaemon) + endpoint = ServiceEndpoint( + Looper.getMainLooper(), + daemonInstance.intermittentDaemon, + connectivityListener + ) notificationManager.acknowledgeStartForegroundService() @@ -243,12 +247,14 @@ class MullvadVpnService : TalpidVpnService() { handlePendingAction(connectionProxy, settings) + endpoint.locationInfoCache.stateEvents = connectionProxy.onStateChange + if (state == State.Running) { instance = ServiceInstance( endpoint.messenger, daemon, + daemonInstance.intermittentDaemon, connectionProxy, - connectivityListener, customDns, endpoint.settingsListener, splitTunneling diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/ServiceInstance.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/ServiceInstance.kt index a124acfa64..b09a80696e 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/ServiceInstance.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/ServiceInstance.kt @@ -2,26 +2,24 @@ package net.mullvad.mullvadvpn.service import android.os.Messenger import net.mullvad.mullvadvpn.service.endpoint.SettingsListener -import net.mullvad.talpid.ConnectivityListener +import net.mullvad.mullvadvpn.util.Intermittent class ServiceInstance( val messenger: Messenger, val daemon: MullvadDaemon, + val intermittentDaemon: Intermittent<MullvadDaemon>, val connectionProxy: ConnectionProxy, - val connectivityListener: ConnectivityListener, val customDns: CustomDns, val settingsListener: SettingsListener, val splitTunneling: SplitTunneling ) { val accountCache = AccountCache(daemon, settingsListener) val keyStatusListener = KeyStatusListener(daemon) - val locationInfoCache = LocationInfoCache(daemon, connectionProxy, connectivityListener) fun onDestroy() { accountCache.onDestroy() connectionProxy.onDestroy() customDns.onDestroy() keyStatusListener.onDestroy() - locationInfoCache.onDestroy() } } diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/LocationInfoCache.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/LocationInfoCache.kt new file mode 100644 index 0000000000..2f118ede6d --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/LocationInfoCache.kt @@ -0,0 +1,140 @@ +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.sendBlocking +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.receiveAsFlow +import net.mullvad.mullvadvpn.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.util.ExponentialBackoff +import net.mullvad.talpid.tunnel.ActionAfterDisconnect +import net.mullvad.talpid.util.autoSubscribable + +class LocationInfoCache(private val endpoint: ServiceEndpoint) { + companion object { + private enum class RequestFetch { + ForRealLocation, + ForRelayLocation, + } + } + + 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>(TunnelState.Disconnected) { _, _, newState -> + when (newState) { + is TunnelState.Disconnected -> { + location = lastKnownRealLocation + fetchRequestChannel.sendBlocking(RequestFetch.ForRealLocation) + } + is TunnelState.Connecting -> location = newState.location + is TunnelState.Connected -> { + location = newState.location + fetchRequestChannel.sendBlocking(RequestFetch.ForRelayLocation) + } + is TunnelState.Disconnecting -> { + when (newState.actionAfterDisconnect) { + ActionAfterDisconnect.Nothing -> location = lastKnownRealLocation + ActionAfterDisconnect.Block -> location = null + ActionAfterDisconnect.Reconnect -> location = selectedRelayLocation + } + } + is TunnelState.Error -> location = null + } + } + + var stateEvents by autoSubscribable<TunnelState>(this, TunnelState.Disconnected) { newState -> + state = newState + } + + init { + endpoint.connectivityListener.connectivityNotifier.subscribe(this) { isConnected -> + if (isConnected && state is TunnelState.Disconnected) { + fetchRequestChannel.sendBlocking(RequestFetch.ForRealLocation) + } + } + + endpoint.settingsListener.relaySettingsNotifier.subscribe(this, ::updateSelectedLocation) + } + + fun onDestroy() { + endpoint.connectivityListener.connectivityNotifier.unsubscribe(this) + endpoint.settingsListener.relaySettingsNotifier.unsubscribe(this) + stateEvents = null + + fetchRequestChannel.close() + } + + private fun runFetcher() = GlobalScope.actor<RequestFetch>( + Dispatchers.Default, + Channel.CONFLATED + ) { + try { + fetcherLoop(channel) + } catch (exception: ClosedReceiveChannelException) { + } + } + + private suspend fun fetcherLoop(channel: ReceiveChannel<RequestFetch>) { + 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<GeoIpLocation, RequestFetch>) { + 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?.location + } +} diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/ServiceEndpoint.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/ServiceEndpoint.kt index b9258c50ac..5f0ecc5ecd 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/ServiceEndpoint.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/ServiceEndpoint.kt @@ -15,10 +15,12 @@ import net.mullvad.mullvadvpn.ipc.Event import net.mullvad.mullvadvpn.ipc.Request import net.mullvad.mullvadvpn.service.MullvadDaemon import net.mullvad.mullvadvpn.util.Intermittent +import net.mullvad.talpid.ConnectivityListener class ServiceEndpoint( looper: Looper, - internal val intermittentDaemon: Intermittent<MullvadDaemon> + internal val intermittentDaemon: Intermittent<MullvadDaemon>, + val connectivityListener: ConnectivityListener ) { private val listeners = mutableSetOf<Messenger>() private val registrationQueue: SendChannel<Messenger> = startRegistrator() @@ -31,6 +33,8 @@ class ServiceEndpoint( val settingsListener = SettingsListener(this) + val locationInfoCache = LocationInfoCache(this) + init { dispatcher.registerHandler(Request.RegisterListener::class) { request -> registrationQueue.sendBlocking(request.listener) @@ -41,6 +45,7 @@ class ServiceEndpoint( dispatcher.onDestroy() registrationQueue.close() + locationInfoCache.onDestroy() settingsListener.onDestroy() } @@ -83,6 +88,7 @@ class ServiceEndpoint( val initialEvents = listOf( Event.SettingsUpdate(settingsListener.settings), + Event.NewLocation(locationInfoCache.location), Event.ListenerReady ) diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/ConnectFragment.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/ConnectFragment.kt index 755a6c5fea..9c8e797945 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/ConnectFragment.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/ConnectFragment.kt @@ -91,7 +91,6 @@ class ConnectFragment : relayListListener.onRelayListChange = { _, selectedRelayItem -> jobTracker.newUiJob("updateSelectedRelayItem") { - locationInfoCache.selectedRelay = selectedRelayItem switchLocationButton.location = selectedRelayItem } } diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/ServiceDependentFragment.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/ServiceDependentFragment.kt index 6ff8b8c488..5e5ecba072 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/ServiceDependentFragment.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/ServiceDependentFragment.kt @@ -11,9 +11,9 @@ import net.mullvad.mullvadvpn.service.AccountCache import net.mullvad.mullvadvpn.service.ConnectionProxy import net.mullvad.mullvadvpn.service.CustomDns import net.mullvad.mullvadvpn.service.KeyStatusListener -import net.mullvad.mullvadvpn.service.LocationInfoCache import net.mullvad.mullvadvpn.service.MullvadDaemon import net.mullvad.mullvadvpn.service.SplitTunneling +import net.mullvad.mullvadvpn.ui.serviceconnection.LocationInfoCache import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnection import net.mullvad.mullvadvpn.ui.serviceconnection.SettingsListener diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/LocationInfoCache.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/LocationInfoCache.kt new file mode 100644 index 0000000000..eb3a8c736a --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/LocationInfoCache.kt @@ -0,0 +1,26 @@ +package net.mullvad.mullvadvpn.ui.serviceconnection + +import kotlin.properties.Delegates.observable +import net.mullvad.mullvadvpn.ipc.DispatchingHandler +import net.mullvad.mullvadvpn.ipc.Event +import net.mullvad.mullvadvpn.model.GeoIpLocation + +class LocationInfoCache(val eventDispatcher: DispatchingHandler<Event>) { + 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/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnection.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnection.kt index 9bc22dd069..da0e09b703 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnection.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnection.kt @@ -26,7 +26,7 @@ class ServiceConnection(private val service: ServiceInstance, val mainActivity: val connectionProxy = service.connectionProxy val customDns = service.customDns val keyStatusListener = service.keyStatusListener - val locationInfoCache = service.locationInfoCache + val locationInfoCache = LocationInfoCache(dispatcher) val settingsListener = SettingsListener(dispatcher) val splitTunneling = service.splitTunneling @@ -42,6 +42,7 @@ class ServiceConnection(private val service: ServiceInstance, val mainActivity: fun onDestroy() { dispatcher.onDestroy() + locationInfoCache.onDestroy() settingsListener.onDestroy() appVersionInfoCache.onDestroy() |
