diff options
15 files changed, 285 insertions, 33 deletions
diff --git a/android/build.gradle b/android/build.gradle index 60b831d41c..6271a20bef 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,6 +1,7 @@ apply plugin: 'com.android.application' apply plugin: 'com.github.triplet.play' apply plugin: 'kotlin-android' +apply plugin: 'kotlin-parcelize' def repoRootPath = projectDir.absoluteFile.parentFile.absolutePath def extraAssetsDirectory = "$project.buildDir/extraAssets" @@ -120,7 +121,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:4.1.2' classpath 'com.github.triplet.gradle:play-publisher:2.7.5' - classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.10' + classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.20' } } diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/DispatchingHandler.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/DispatchingHandler.kt new file mode 100644 index 0000000000..0f3820ff48 --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/DispatchingHandler.kt @@ -0,0 +1,48 @@ +package net.mullvad.mullvadvpn.ipc + +import android.os.Handler +import android.os.Looper +import android.os.Message +import android.util.Log +import java.util.concurrent.locks.ReentrantReadWriteLock +import kotlin.concurrent.withLock +import kotlin.reflect.KClass + +class DispatchingHandler<T : Any>( + looper: Looper, + private val extractor: (Message) -> T? +) : Handler(looper) { + private val handlers = HashMap<KClass<out T>, (T) -> Unit>() + private val lock = ReentrantReadWriteLock() + + fun <V : T> registerHandler(variant: KClass<V>, handler: (V) -> Unit) { + lock.writeLock().withLock { + handlers.put(variant) { instance -> + @Suppress("UNCHECKED_CAST") + handler(instance as V) + } + } + } + + override fun handleMessage(message: Message) { + lock.readLock().withLock { + val instance = extractor(message) + + if (instance != null) { + val handler = handlers.get(instance::class) + + handler?.invoke(instance) + } else { + Log.e("mullvad", "Dispatching handler received an unexpected message") + } + } + } + + fun onDestroy() { + lock.writeLock().withLock { + handlers.clear() + } + + removeCallbacksAndMessages(null) + } +} diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Event.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Event.kt new file mode 100644 index 0000000000..fa6aa22081 --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Event.kt @@ -0,0 +1,20 @@ +package net.mullvad.mullvadvpn.ipc + +import android.os.Message as RawMessage +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +// Events that can be sent from the service +sealed class Event : Message(), Parcelable { + protected override val messageId = 1 + protected override val messageKey = MESSAGE_KEY + + @Parcelize + object ListenerReady : Event(), Parcelable + + companion object { + private const val MESSAGE_KEY = "event" + + fun fromMessage(message: RawMessage): Event? = Message.fromMessage(message, MESSAGE_KEY) + } +} diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Message.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Message.kt new file mode 100644 index 0000000000..872acba8e7 --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Message.kt @@ -0,0 +1,25 @@ +package net.mullvad.mullvadvpn.ipc + +import android.os.Bundle +import android.os.Message as RawMessage +import android.os.Parcelable + +abstract class Message : Parcelable { + protected abstract val messageId: Int + protected abstract val messageKey: String + + val message: RawMessage + get() = RawMessage.obtain().also { message -> + message.what = messageId + message.data = Bundle() + message.data.putParcelable(messageKey, this) + } + + companion object { + internal fun <T : Parcelable> fromMessage(message: RawMessage, key: String): T? { + val data = message.data + + return data.getParcelable(key) + } + } +} diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Request.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Request.kt new file mode 100644 index 0000000000..47b613816b --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Request.kt @@ -0,0 +1,21 @@ +package net.mullvad.mullvadvpn.ipc + +import android.os.Message as RawMessage +import android.os.Messenger +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +// Requests that the service can handle +sealed class Request : Message(), Parcelable { + protected override val messageId = 2 + protected override val messageKey = MESSAGE_KEY + + @Parcelize + class RegisterListener(val listener: Messenger) : Request(), Parcelable + + companion object { + private const val MESSAGE_KEY = "request" + + fun fromMessage(message: RawMessage): Request? = Message.fromMessage(message, MESSAGE_KEY) + } +} 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 052f65ebd3..0e43e7aa64 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadVpnService.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadVpnService.kt @@ -6,6 +6,7 @@ import android.content.Intent import android.net.VpnService import android.os.Binder import android.os.IBinder +import android.os.Looper import android.util.Log import kotlin.properties.Delegates.observable import kotlinx.coroutines.CompletableDeferred @@ -14,6 +15,7 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.Job import kotlinx.coroutines.launch import net.mullvad.mullvadvpn.model.Settings +import net.mullvad.mullvadvpn.service.endpoint.ServiceEndpoint import net.mullvad.mullvadvpn.service.notifications.AccountExpiryNotification import net.mullvad.mullvadvpn.service.tunnelstate.TunnelStateUpdater import net.mullvad.mullvadvpn.ui.MainActivity @@ -69,6 +71,7 @@ class MullvadVpnService : TalpidVpnService() { } private lateinit var daemonInstance: DaemonInstance + private lateinit var endpoint: ServiceEndpoint private lateinit var keyguardManager: KeyguardManager private lateinit var notificationManager: ForegroundNotificationManager private lateinit var tunnelStateUpdater: TunnelStateUpdater @@ -98,13 +101,16 @@ class MullvadVpnService : TalpidVpnService() { initializeSplitTunneling() + daemonInstance = DaemonInstance(this) keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager notificationManager = ForegroundNotificationManager(this, serviceNotifier, keyguardManager) tunnelStateUpdater = TunnelStateUpdater(this, serviceNotifier) + endpoint = ServiceEndpoint(Looper.getMainLooper(), daemonInstance.intermittentDaemon) + notificationManager.acknowledgeStartForegroundService() - daemonInstance = DaemonInstance(this).apply { + daemonInstance.apply { intermittentDaemon.registerListener(this@MullvadVpnService) { daemon -> handleDaemonInstance(daemon) } @@ -241,6 +247,7 @@ class MullvadVpnService : TalpidVpnService() { if (state == State.Running) { instance = ServiceInstance( + endpoint.messenger, daemon, connectionProxy, connectivityListener, 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 135ea9ef28..19d4443162 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/ServiceInstance.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/ServiceInstance.kt @@ -1,8 +1,10 @@ package net.mullvad.mullvadvpn.service +import android.os.Messenger import net.mullvad.talpid.ConnectivityListener class ServiceInstance( + val messenger: Messenger, val daemon: MullvadDaemon, val connectionProxy: ConnectionProxy, val connectivityListener: ConnectivityListener, 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 new file mode 100644 index 0000000000..6541ef2ba7 --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/ServiceEndpoint.kt @@ -0,0 +1,84 @@ +package net.mullvad.mullvadvpn.service.endpoint + +import android.os.DeadObjectException +import android.os.Looper +import android.os.Messenger +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.ClosedReceiveChannelException +import kotlinx.coroutines.channels.SendChannel +import kotlinx.coroutines.channels.actor +import kotlinx.coroutines.channels.sendBlocking +import net.mullvad.mullvadvpn.ipc.DispatchingHandler +import net.mullvad.mullvadvpn.ipc.Event +import net.mullvad.mullvadvpn.ipc.Request +import net.mullvad.mullvadvpn.service.MullvadDaemon +import net.mullvad.mullvadvpn.util.Intermittent + +class ServiceEndpoint(looper: Looper, private val intermittentDaemon: Intermittent<MullvadDaemon>) { + private val listeners = mutableSetOf<Messenger>() + private val registrationQueue: SendChannel<Messenger> = startRegistrator() + + internal val dispatcher = DispatchingHandler(looper) { message -> + Request.fromMessage(message) + } + + val messenger = Messenger(dispatcher) + + init { + dispatcher.registerHandler(Request.RegisterListener::class) { request -> + registrationQueue.sendBlocking(request.listener) + } + } + + fun onDestroy() { + dispatcher.onDestroy() + registrationQueue.close() + } + + internal fun sendEvent(event: Event) { + synchronized(this) { + val deadListeners = mutableSetOf<Messenger>() + + for (listener in listeners) { + try { + listener.send(event.message) + } catch (_: DeadObjectException) { + deadListeners.add(listener) + } + } + + deadListeners.forEach { listeners.remove(it) } + } + } + + private fun startRegistrator() = GlobalScope.actor<Messenger>( + Dispatchers.Default, + Channel.UNLIMITED + ) { + try { + while (true) { + val listener = channel.receive() + + intermittentDaemon.await() + + registerListener(listener) + } + } catch (exception: ClosedReceiveChannelException) { + // Registration queue closed; stop registrator + } + } + + private fun registerListener(listener: Messenger) { + synchronized(this) { + listeners.add(listener) + + val initialEvents = listOf(Event.ListenerReady) + + initialEvents.forEach { event -> + listener.send(event.message) + } + } + } +} diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/LaunchFragment.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/LaunchFragment.kt index 795a3bddb7..ac4470520f 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/LaunchFragment.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/LaunchFragment.kt @@ -6,6 +6,7 @@ import android.view.View import android.view.ViewGroup import kotlinx.coroutines.CompletableDeferred import net.mullvad.mullvadvpn.R +import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnection class LaunchFragment : ServiceAwareFragment() { private val hasAccountToken = CompletableDeferred<Boolean>() diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt index 5929490065..2d597929dd 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt @@ -19,7 +19,9 @@ import kotlinx.coroutines.launch import net.mullvad.mullvadvpn.BuildConfig import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.dataproxy.MullvadProblemReport +import net.mullvad.mullvadvpn.ipc.Event import net.mullvad.mullvadvpn.service.MullvadVpnService +import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnection import net.mullvad.talpid.util.EventNotifier class MainActivity : FragmentActivity() { @@ -60,7 +62,14 @@ class MainActivity : FragmentActivity() { } serviceConnection = newConnection - serviceNotifier.notify(newConnection) + + if (newConnection != null) { + newConnection.dispatcher.registerHandler(Event.ListenerReady::class) { _ -> + serviceNotifier.notify(newConnection) + } + } else { + serviceNotifier.notify(null) + } if (shouldConnect) { tryToConnect() @@ -71,6 +80,7 @@ class MainActivity : FragmentActivity() { override fun onServiceDisconnected(className: ComponentName) { android.util.Log.d("mullvad", "UI lost the connection to the service") service?.serviceNotifier?.unsubscribe(this@MainActivity) + serviceConnection?.onDestroy() service = null serviceConnection = null serviceNotifier.notify(null) diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/ServiceAwareFragment.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/ServiceAwareFragment.kt index a1003f511d..5788c60ad8 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/ServiceAwareFragment.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/ServiceAwareFragment.kt @@ -2,6 +2,7 @@ package net.mullvad.mullvadvpn.ui import android.content.Context import net.mullvad.mullvadvpn.ui.fragments.BaseFragment +import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnection import net.mullvad.mullvadvpn.util.JobTracker abstract class ServiceAwareFragment : BaseFragment() { diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/ServiceConnection.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/ServiceConnection.kt deleted file mode 100644 index 43a0e24dc8..0000000000 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/ServiceConnection.kt +++ /dev/null @@ -1,30 +0,0 @@ -package net.mullvad.mullvadvpn.ui - -import net.mullvad.mullvadvpn.dataproxy.AppVersionInfoCache -import net.mullvad.mullvadvpn.dataproxy.RelayListListener -import net.mullvad.mullvadvpn.service.ServiceInstance - -class ServiceConnection(private val service: ServiceInstance, val mainActivity: MainActivity) { - val daemon = service.daemon - val accountCache = service.accountCache - val connectionProxy = service.connectionProxy - val customDns = service.customDns - val keyStatusListener = service.keyStatusListener - val locationInfoCache = service.locationInfoCache - val settingsListener = service.settingsListener - val splitTunneling = service.splitTunneling - - val appVersionInfoCache = AppVersionInfoCache(mainActivity, daemon, settingsListener) - var relayListListener = RelayListListener(daemon, settingsListener) - - init { - appVersionInfoCache.onCreate() - connectionProxy.mainActivity = mainActivity - } - - fun onDestroy() { - appVersionInfoCache.onDestroy() - relayListListener.onDestroy() - connectionProxy.mainActivity = null - } -} 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 e523ba481b..30c0a6db30 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/ServiceDependentFragment.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/ServiceDependentFragment.kt @@ -15,6 +15,7 @@ import net.mullvad.mullvadvpn.service.LocationInfoCache import net.mullvad.mullvadvpn.service.MullvadDaemon import net.mullvad.mullvadvpn.service.SettingsListener import net.mullvad.mullvadvpn.service.SplitTunneling +import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnection abstract class ServiceDependentFragment(val onNoService: OnNoService) : ServiceAwareFragment() { enum class OnNoService { diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/SettingsFragment.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/SettingsFragment.kt index 6725430052..4f823fada2 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/SettingsFragment.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/SettingsFragment.kt @@ -8,6 +8,7 @@ import android.widget.ImageButton import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.dataproxy.AppVersionInfoCache import net.mullvad.mullvadvpn.service.AccountCache +import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnection import net.mullvad.mullvadvpn.ui.widget.AccountCell import net.mullvad.mullvadvpn.ui.widget.AppVersionCell import net.mullvad.mullvadvpn.ui.widget.NavigateCell 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 new file mode 100644 index 0000000000..d9071ca2b9 --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnection.kt @@ -0,0 +1,60 @@ +package net.mullvad.mullvadvpn.ui.serviceconnection + +import android.os.Looper +import android.os.Messenger +import android.os.RemoteException +import android.util.Log +import net.mullvad.mullvadvpn.dataproxy.AppVersionInfoCache +import net.mullvad.mullvadvpn.dataproxy.RelayListListener +import net.mullvad.mullvadvpn.ipc.DispatchingHandler +import net.mullvad.mullvadvpn.ipc.Event +import net.mullvad.mullvadvpn.ipc.Request +import net.mullvad.mullvadvpn.service.ServiceInstance +import net.mullvad.mullvadvpn.ui.MainActivity + +// Container of classes that communicate with the service through an active connection +// +// The properties of this class can be used to send events to the service, to listen for events from +// the service and to get values received from events. +class ServiceConnection(private val service: ServiceInstance, val mainActivity: MainActivity) { + val dispatcher = DispatchingHandler(Looper.getMainLooper()) { message -> + Event.fromMessage(message) + } + + val daemon = service.daemon + val accountCache = service.accountCache + val connectionProxy = service.connectionProxy + val customDns = service.customDns + val keyStatusListener = service.keyStatusListener + val locationInfoCache = service.locationInfoCache + val settingsListener = service.settingsListener + val splitTunneling = service.splitTunneling + + val appVersionInfoCache = AppVersionInfoCache(mainActivity, daemon, settingsListener) + var relayListListener = RelayListListener(daemon, settingsListener) + + init { + appVersionInfoCache.onCreate() + connectionProxy.mainActivity = mainActivity + registerListener() + } + + fun onDestroy() { + dispatcher.onDestroy() + + appVersionInfoCache.onDestroy() + relayListListener.onDestroy() + connectionProxy.mainActivity = null + } + + private fun registerListener() { + val listener = Messenger(dispatcher) + val request = Request.RegisterListener(listener) + + try { + service.messenger.send(request.message) + } catch (exception: RemoteException) { + Log.e("mullvad", "Failed to register listener for service events", exception) + } + } +} |
