diff options
| author | Aleksandr Granin <aleksandr@mullvad.net> | 2021-04-08 13:35:29 +0200 |
|---|---|---|
| committer | Aleksandr Granin <aleksandr@mullvad.net> | 2021-04-08 13:35:29 +0200 |
| commit | eb0c1ddff764f77c3960b041e63527cfbbe668fb (patch) | |
| tree | 504d0182bc9f1c431e7bfa77f3fb13f20717c6ce | |
| parent | 6979590d25be6607fb87954c7dfbfc2b7192868f (diff) | |
| parent | df33a7600b3f3e1976ba2ab5e7b43d4cbedc169a (diff) | |
| download | mullvadvpn-eb0c1ddff764f77c3960b041e63527cfbbe668fb.tar.xz mullvadvpn-eb0c1ddff764f77c3960b041e63527cfbbe668fb.zip | |
Merge branch 'split-split-tunneling'
21 files changed, 281 insertions, 126 deletions
diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/applist/AppListAdapter.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/applist/AppListAdapter.kt index c82ad71dad..adbafc3dbd 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/applist/AppListAdapter.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/applist/AppListAdapter.kt @@ -9,7 +9,7 @@ import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView.Adapter import kotlin.properties.Delegates.observable import net.mullvad.mullvadvpn.R -import net.mullvad.mullvadvpn.service.SplitTunneling +import net.mullvad.mullvadvpn.ui.serviceconnection.SplitTunneling import net.mullvad.mullvadvpn.util.JobTracker class AppListAdapter( diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/applist/AppListItemHolder.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/applist/AppListItemHolder.kt index 2a9a6249dd..6f52604018 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/applist/AppListItemHolder.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/applist/AppListItemHolder.kt @@ -8,7 +8,7 @@ import android.widget.TextView import androidx.recyclerview.widget.RecyclerView.ViewHolder import kotlin.properties.Delegates.observable import net.mullvad.mullvadvpn.R -import net.mullvad.mullvadvpn.service.SplitTunneling +import net.mullvad.mullvadvpn.ui.serviceconnection.SplitTunneling import net.mullvad.mullvadvpn.ui.widget.CellSwitch import net.mullvad.mullvadvpn.util.JobTracker diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/di/AppModule.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/di/AppModule.kt index a144ae6bc2..87aabc6d32 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/di/AppModule.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/di/AppModule.kt @@ -1,10 +1,16 @@ package net.mullvad.mullvadvpn.di import android.content.pm.PackageManager +import android.os.Messenger import kotlinx.coroutines.Dispatchers import net.mullvad.mullvadvpn.applist.ApplicationsIconManager import net.mullvad.mullvadvpn.applist.ApplicationsProvider -import net.mullvad.mullvadvpn.service.SplitTunneling +import net.mullvad.mullvadvpn.ipc.Event +import net.mullvad.mullvadvpn.ipc.MessageDispatcher +import net.mullvad.mullvadvpn.service.ServiceInstance +import net.mullvad.mullvadvpn.ui.MainActivity +import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnection +import net.mullvad.mullvadvpn.ui.serviceconnection.SplitTunneling import net.mullvad.mullvadvpn.viewmodel.SplitTunnelingViewModel import org.koin.android.ext.koin.androidContext import org.koin.androidx.viewmodel.dsl.viewModel @@ -14,7 +20,6 @@ import org.koin.dsl.onClose val appModule = module { - single<SplitTunneling> { SplitTunneling(androidContext()) } single<PackageManager> { androidContext().packageManager } single<String> (named(SELF_PACKAGE_NAME)) { androidContext().packageName } @@ -23,6 +28,15 @@ val appModule = module { scoped { ApplicationsIconManager(get()) } onClose { it?.dispose() } scoped { ApplicationsProvider(get(), get(named(SELF_PACKAGE_NAME))) } } + + scope<ServiceConnection> { + scoped<ServiceConnection> { (service: ServiceInstance, mainActivity: MainActivity) -> + ServiceConnection(service, mainActivity) + } onClose { it?.onDestroy() } + scoped<SplitTunneling> { (messenger: Messenger, dispatcher: MessageDispatcher<Event>) -> + SplitTunneling(messenger, dispatcher) + } + } } const val APPS_SCOPE = "APPS_SCOPE" const val SELF_PACKAGE_NAME = "SELF_PACKAGE_NAME" diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/DispatchingHandler.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/DispatchingHandler.kt index 0f3820ff48..93c79a1ab9 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/DispatchingHandler.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/DispatchingHandler.kt @@ -11,11 +11,11 @@ import kotlin.reflect.KClass class DispatchingHandler<T : Any>( looper: Looper, private val extractor: (Message) -> T? -) : Handler(looper) { +) : Handler(looper), MessageDispatcher<T> { private val handlers = HashMap<KClass<out T>, (T) -> Unit>() private val lock = ReentrantReadWriteLock() - fun <V : T> registerHandler(variant: KClass<V>, handler: (V) -> Unit) { + override fun <V : T> registerHandler(variant: KClass<V>, handler: (V) -> Unit) { lock.writeLock().withLock { handlers.put(variant) { instance -> @Suppress("UNCHECKED_CAST") 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 527082d323..285b7abea2 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Event.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Event.kt @@ -27,6 +27,9 @@ sealed class Event : Message.EventMessage() { data class SettingsUpdate(val settings: Settings?) : Event() @Parcelize + data class SplitTunnelingUpdate(val excludedApps: List<String>?) : Event() + + @Parcelize data class WireGuardKeyStatus(val keyStatus: KeygenEvent?) : Event() companion object { diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/MessageDispatcher.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/MessageDispatcher.kt new file mode 100644 index 0000000000..8a681b2ce4 --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/MessageDispatcher.kt @@ -0,0 +1,7 @@ +package net.mullvad.mullvadvpn.ipc + +import kotlin.reflect.KClass + +interface MessageDispatcher<T : Any> { + fun <V : T> registerHandler(variant: KClass<V>, handler: (V) -> Unit) +} 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 c6d03bc4b4..b8dfc3c3dd 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Request.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Request.kt @@ -13,9 +13,15 @@ sealed class Request : Message.RequestMessage() { object CreateAccount : Request() @Parcelize + data class ExcludeApp(val packageName: String) : Request() + + @Parcelize object FetchAccountExpiry : Request() @Parcelize + data class IncludeApp(val packageName: String) : Request() + + @Parcelize data class InvalidateAccountExpiry(val expiry: DateTime) : Request() @Parcelize @@ -25,12 +31,18 @@ sealed class Request : Message.RequestMessage() { object Logout : Request() @Parcelize + object PersistExcludedApps : Request() + + @Parcelize data class RegisterListener(val listener: Messenger) : Request() @Parcelize data class RemoveAccountFromHistory(val account: String?) : Request() @Parcelize + data class SetEnableSplitTunneling(val enable: Boolean) : Request() + + @Parcelize object WireGuardGenerateKey : Request() @Parcelize diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/DaemonInstance.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/DaemonInstance.kt index a69715653b..23b127addf 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/DaemonInstance.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/DaemonInstance.kt @@ -45,7 +45,6 @@ class DaemonInstance(val vpnService: MullvadVpnService) { var isRunning = true prepareFiles() - vpnService.splitTunneling.join() while (isRunning) { if (!waitForCommand(channel, Command.START)) { 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 946568a83d..bca3f4956d 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadVpnService.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadVpnService.kt @@ -9,7 +9,6 @@ import android.os.IBinder import android.os.Looper import android.util.Log import kotlin.properties.Delegates.observable -import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.Job @@ -17,6 +16,7 @@ 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.persistence.SplitTunnelingPersistence import net.mullvad.mullvadvpn.service.tunnelstate.TunnelStateUpdater import net.mullvad.mullvadvpn.ui.MainActivity import net.mullvad.talpid.TalpidVpnService @@ -77,9 +77,13 @@ class MullvadVpnService : TalpidVpnService() { private lateinit var tunnelStateUpdater: TunnelStateUpdater private var pendingAction by observable<PendingAction?>(null) { _, _, _ -> - instance?.let { activeInstance -> - endpoint.settingsListener.settings?.let { currentSettings -> - handlePendingAction(activeInstance.connectionProxy, currentSettings) + val connectionProxy = instance?.connectionProxy + + // The service instance awaits the split tunneling initialization, which also starts the + // endpoint. So if the instance is not null, the endpoint has certainly been initialized. + if (connectionProxy != null) { + endpoint.settingsListener.settings?.let { settings -> + handlePendingAction(connectionProxy, settings) } } } @@ -92,14 +96,10 @@ class MullvadVpnService : TalpidVpnService() { notificationManager.lockedToForeground = isUiVisible or isBound } - internal val splitTunneling = CompletableDeferred<SplitTunneling>() - override fun onCreate() { super.onCreate() Log.d(TAG, "Initializing service") - initializeSplitTunneling() - daemonInstance = DaemonInstance(this) keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager tunnelStateUpdater = TunnelStateUpdater(this, serviceNotifier) @@ -107,12 +107,14 @@ class MullvadVpnService : TalpidVpnService() { endpoint = ServiceEndpoint( Looper.getMainLooper(), daemonInstance.intermittentDaemon, - connectivityListener + connectivityListener, + SplitTunnelingPersistence(this) ) notificationManager = ForegroundNotificationManager(this, serviceNotifier, keyguardManager).apply { acknowledgeStartForegroundService() + accountNumberEvents = endpoint.settingsListener.accountNumberNotifier } daemonInstance.apply { @@ -200,17 +202,6 @@ class MullvadVpnService : TalpidVpnService() { set(value) { this@MullvadVpnService.isUiVisible = value } } - private fun initializeSplitTunneling() = GlobalScope.launch(Dispatchers.Default) { - splitTunneling.complete( - SplitTunneling(this@MullvadVpnService).apply { - onChange = { excludedApps -> - disallowedApps = excludedApps - markTunAsStale() - } - } - ) - } - private fun handleDaemonInstance(daemon: MullvadDaemon?) { setUpDaemonJob?.cancel() @@ -226,24 +217,23 @@ class MullvadVpnService : TalpidVpnService() { } } - private fun setUpDaemon(daemon: MullvadDaemon) = GlobalScope.launch(Dispatchers.Default) { - val settings = daemon.getSettings() + private fun setUpDaemon(daemon: MullvadDaemon) = GlobalScope.launch(Dispatchers.Main) { + if (state != State.Stopped) { + val settings = daemon.getSettings() - if (settings != null) { - setUpInstance(daemon, settings) - } else { - restart() + if (settings != null) { + setUpInstance(daemon, settings) + } else { + restart() + } } } private suspend fun setUpInstance(daemon: MullvadDaemon, settings: Settings) { val connectionProxy = ConnectionProxy(this, daemon) val customDns = CustomDns(daemon, endpoint.settingsListener) - val splitTunneling = splitTunneling.await() - - notificationManager.accountNumberEvents = endpoint.settingsListener.accountNumberNotifier - splitTunneling.onChange = { excludedApps -> + endpoint.splitTunneling.onChange.subscribe(this@MullvadVpnService) { excludedApps -> disallowedApps = excludedApps markTunAsStale() connectionProxy.reconnect() @@ -259,8 +249,7 @@ class MullvadVpnService : TalpidVpnService() { daemon, daemonInstance.intermittentDaemon, connectionProxy, - customDns, - splitTunneling + customDns ) } } 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 483fbce6e5..f97d8c870f 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/ServiceInstance.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/ServiceInstance.kt @@ -9,7 +9,6 @@ class ServiceInstance( val intermittentDaemon: Intermittent<MullvadDaemon>, val connectionProxy: ConnectionProxy, val customDns: CustomDns, - val splitTunneling: SplitTunneling ) { fun onDestroy() { connectionProxy.onDestroy() diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/SplitTunneling.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/SplitTunneling.kt deleted file mode 100644 index 78015e4e4b..0000000000 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/SplitTunneling.kt +++ /dev/null @@ -1,69 +0,0 @@ -package net.mullvad.mullvadvpn.service - -import android.content.Context -import java.io.File -import kotlin.properties.Delegates.observable - -// The spelling of the shared preferences location can't be changed to American English without -// either having users lose their preferences on update or implementing some migration code. -private const val SHARED_PREFERENCES = "split_tunnelling" -private const val KEY_ENABLED = "enabled" - -class SplitTunneling(context: Context) { - // The spelling of the app list file name can't be changed to American English without either - // having users lose their preferences on update or implementing some migration code. - private val appListFile = File(context.filesDir, "split-tunnelling.txt") - private val excludedApps = HashSet<String>() - private val preferences = context.getSharedPreferences(SHARED_PREFERENCES, Context.MODE_PRIVATE) - - val excludedAppList - get() = if (enabled) { - excludedApps.toList() - } else { - emptyList() - } - - var enabled by observable(preferences.getBoolean(KEY_ENABLED, false)) { _, _, _ -> - enabledChanged() - } - - var onChange by observable<((List<String>) -> Unit)?>(null) { _, _, _ -> - update() - } - - init { - if (appListFile.exists()) { - excludedApps.addAll(appListFile.readLines()) - update() - } - } - - fun isAppExcluded(appPackageName: String) = excludedApps.contains(appPackageName) - - fun excludeApp(appPackageName: String) { - excludedApps.add(appPackageName) - update() - } - - fun includeApp(appPackageName: String) { - excludedApps.remove(appPackageName) - update() - } - - fun persist() { - appListFile.writeText(excludedApps.joinToString(separator = "\n")) - } - - private fun enabledChanged() { - preferences.edit().apply { - putBoolean(KEY_ENABLED, enabled) - apply() - } - - update() - } - - private fun update() { - onChange?.invoke(excludedAppList) - } -} 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 70dd295d5f..8e11e1e1cd 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 @@ -14,13 +14,15 @@ 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.service.persistence.SplitTunnelingPersistence import net.mullvad.mullvadvpn.util.Intermittent import net.mullvad.talpid.ConnectivityListener class ServiceEndpoint( looper: Looper, internal val intermittentDaemon: Intermittent<MullvadDaemon>, - val connectivityListener: ConnectivityListener + val connectivityListener: ConnectivityListener, + splitTunnelingPersistence: SplitTunnelingPersistence ) { private val listeners = mutableSetOf<Messenger>() private val registrationQueue: SendChannel<Messenger> = startRegistrator() @@ -36,6 +38,7 @@ class ServiceEndpoint( val accountCache = AccountCache(this) val keyStatusListener = KeyStatusListener(this) val locationInfoCache = LocationInfoCache(this) + val splitTunneling = SplitTunneling(splitTunnelingPersistence, this) init { dispatcher.registerHandler(Request.RegisterListener::class) { request -> @@ -51,6 +54,7 @@ class ServiceEndpoint( keyStatusListener.onDestroy() locationInfoCache.onDestroy() settingsListener.onDestroy() + splitTunneling.onDestroy() } internal fun sendEvent(event: Event) { @@ -96,6 +100,7 @@ class ServiceEndpoint( Event.SettingsUpdate(settingsListener.settings), Event.NewLocation(locationInfoCache.location), Event.WireGuardKeyStatus(keyStatusListener.keyStatus), + Event.SplitTunnelingUpdate(splitTunneling.onChange.latestEvent), Event.ListenerReady ) diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/SplitTunneling.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/SplitTunneling.kt new file mode 100644 index 0000000000..f9b77704c6 --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/SplitTunneling.kt @@ -0,0 +1,56 @@ +package net.mullvad.mullvadvpn.service.endpoint + +import kotlin.properties.Delegates.observable +import net.mullvad.mullvadvpn.ipc.Event +import net.mullvad.mullvadvpn.ipc.Request +import net.mullvad.mullvadvpn.service.persistence.SplitTunnelingPersistence +import net.mullvad.talpid.util.EventNotifier + +class SplitTunneling(persistence: SplitTunnelingPersistence, endpoint: ServiceEndpoint) { + private val excludedApps = persistence.excludedApps.toMutableSet() + + private var enabled by observable(persistence.enabled) { _, _, isEnabled -> + persistence.enabled = isEnabled + update() + } + + val onChange = EventNotifier<List<String>?>(null) + + init { + onChange.subscribe(this) { excludedApps -> + endpoint.sendEvent(Event.SplitTunnelingUpdate(excludedApps)) + } + + endpoint.dispatcher.apply { + registerHandler(Request.IncludeApp::class) { request -> + excludedApps.remove(request.packageName) + update() + } + + registerHandler(Request.ExcludeApp::class) { request -> + excludedApps.add(request.packageName) + update() + } + + registerHandler(Request.SetEnableSplitTunneling::class) { request -> + enabled = request.enable + } + + registerHandler(Request.PersistExcludedApps::class) { _ -> + persistence.excludedApps = excludedApps + } + } + } + + fun onDestroy() { + onChange.unsubscribeAll() + } + + private fun update() { + if (enabled) { + onChange.notify(excludedApps.toList()) + } else { + onChange.notify(null) + } + } +} diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/service/persistence/SplitTunnelingPersistence.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/persistence/SplitTunnelingPersistence.kt new file mode 100644 index 0000000000..425aec8836 --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/service/persistence/SplitTunnelingPersistence.kt @@ -0,0 +1,35 @@ +package net.mullvad.mullvadvpn.service.persistence + +import android.content.Context +import java.io.File +import kotlin.properties.Delegates.observable + +// The spelling of the shared preferences location can't be changed to American English without +// either having users lose their preferences on update or implementing some migration code. +private const val SHARED_PREFERENCES = "split_tunnelling" +private const val KEY_ENABLED = "enabled" + +class SplitTunnelingPersistence(context: Context) { + // The spelling of the app list file name can't be changed to American English without either + // having users lose their preferences on update or implementing some migration code. + private val appListFile = File(context.filesDir, "split-tunnelling.txt") + private val preferences = context.getSharedPreferences(SHARED_PREFERENCES, Context.MODE_PRIVATE) + + var enabled by observable(preferences.getBoolean(KEY_ENABLED, false)) { _, _, isEnabled -> + preferences.edit().apply { + putBoolean(KEY_ENABLED, isEnabled) + apply() + } + } + + var excludedApps by observable(loadExcludedApps()) { _, _, excludedAppsSet -> + appListFile.writeText(excludedAppsSet.joinToString(separator = "\n")) + } + + private fun loadExcludedApps(): Set<String> { + return when { + appListFile.exists() -> appListFile.readLines().toSet() + else -> emptySet() + } + } +} 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 89df0bbedb..486e1d156c 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt @@ -23,6 +23,9 @@ 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 +import org.koin.android.ext.android.getKoin +import org.koin.core.parameter.parametersOf +import org.koin.core.scope.Scope open class MainActivity : FragmentActivity() { companion object { @@ -44,6 +47,7 @@ open class MainActivity : FragmentActivity() { uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION } + private var serviceConnectionScope: Scope? = null private val serviceConnectionManager = object : android.content.ServiceConnection { override fun onServiceConnected(className: ComponentName, binder: IBinder) { android.util.Log.d("mullvad", "UI successfully connected to the service") @@ -55,10 +59,13 @@ open class MainActivity : FragmentActivity() { localBinder.serviceNotifier.subscribe(this@MainActivity) { service -> android.util.Log.d("mullvad", "UI connection to the service changed: $service") - serviceConnection?.onDestroy() + serviceConnectionScope?.close() val newConnection = service?.let { safeService -> - ServiceConnection(safeService, this@MainActivity) + serviceConnectionScope = getKoin().createScope<ServiceConnection>() + serviceConnectionScope?.get<ServiceConnection>( + parameters = { parametersOf(safeService, this@MainActivity) } + ) } serviceConnection = newConnection @@ -80,7 +87,7 @@ open 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() + serviceConnectionScope?.close() service = null serviceConnection = null serviceNotifier.notify(null) @@ -154,7 +161,7 @@ open class MainActivity : FragmentActivity() { override fun onDestroy() { serviceNotifier.unsubscribeAll() - serviceConnection?.onDestroy() + serviceConnectionScope?.close() super.onDestroy() } 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 c49ac5343a..0cb33c3334 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/ServiceDependentFragment.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/ServiceDependentFragment.kt @@ -10,12 +10,12 @@ import net.mullvad.mullvadvpn.dataproxy.RelayListListener import net.mullvad.mullvadvpn.service.ConnectionProxy import net.mullvad.mullvadvpn.service.CustomDns import net.mullvad.mullvadvpn.service.MullvadDaemon -import net.mullvad.mullvadvpn.service.SplitTunneling import net.mullvad.mullvadvpn.ui.serviceconnection.AccountCache import net.mullvad.mullvadvpn.ui.serviceconnection.KeyStatusListener import net.mullvad.mullvadvpn.ui.serviceconnection.LocationInfoCache import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnection import net.mullvad.mullvadvpn.ui.serviceconnection.SettingsListener +import net.mullvad.mullvadvpn.ui.serviceconnection.SplitTunneling abstract class ServiceDependentFragment(val onNoService: OnNoService) : ServiceAwareFragment() { enum class OnNoService { 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 2d2bad5553..becd03b203 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 @@ -11,12 +11,18 @@ import net.mullvad.mullvadvpn.ipc.Event import net.mullvad.mullvadvpn.ipc.Request import net.mullvad.mullvadvpn.service.ServiceInstance import net.mullvad.mullvadvpn.ui.MainActivity +import org.koin.core.component.KoinApiExtension +import org.koin.core.component.KoinComponent +import org.koin.core.component.get +import org.koin.core.parameter.parametersOf // 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) { +@OptIn(KoinApiExtension::class) +class ServiceConnection(private val service: ServiceInstance, mainActivity: MainActivity) : + KoinComponent { val dispatcher = DispatchingHandler(Looper.getMainLooper()) { message -> Event.fromMessage(message) } @@ -28,7 +34,9 @@ class ServiceConnection(private val service: ServiceInstance, val mainActivity: val keyStatusListener = KeyStatusListener(service.messenger, dispatcher) val locationInfoCache = LocationInfoCache(dispatcher) val settingsListener = SettingsListener(dispatcher) - val splitTunneling = service.splitTunneling + val splitTunneling = get<SplitTunneling>( + parameters = { parametersOf(service.messenger, dispatcher) } + ) val appVersionInfoCache = AppVersionInfoCache(mainActivity, daemon, settingsListener) var relayListListener = RelayListListener(daemon, settingsListener) diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/SplitTunneling.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/SplitTunneling.kt new file mode 100644 index 0000000000..4bcc3e83a1 --- /dev/null +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/SplitTunneling.kt @@ -0,0 +1,41 @@ +package net.mullvad.mullvadvpn.ui.serviceconnection + +import android.os.Messenger +import kotlin.properties.Delegates.observable +import net.mullvad.mullvadvpn.ipc.Event +import net.mullvad.mullvadvpn.ipc.MessageDispatcher +import net.mullvad.mullvadvpn.ipc.Request + +class SplitTunneling( + private val connection: Messenger, + eventDispatcher: MessageDispatcher<Event> +) { + private var excludedApps: Set<String> = emptySet() + + var enabled by observable(false) { _, wasEnabled, isEnabled -> + if (wasEnabled != isEnabled) { + connection.send(Request.SetEnableSplitTunneling(isEnabled).message) + } + } + + init { + eventDispatcher.registerHandler(Event.SplitTunnelingUpdate::class) { event -> + if (event.excludedApps != null) { + enabled = true + excludedApps = event.excludedApps.toSet() + } else { + enabled = false + } + } + } + + fun isAppExcluded(appPackageName: String): Boolean = excludedApps.contains(appPackageName) + + fun excludeApp(appPackageName: String) = + connection.send(Request.ExcludeApp(appPackageName).message) + + fun includeApp(appPackageName: String) = + connection.send(Request.IncludeApp(appPackageName).message) + + fun persist() = connection.send(Request.PersistExcludedApps.message) +} diff --git a/android/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModel.kt b/android/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModel.kt index 98730de960..9e18a90ab7 100644 --- a/android/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModel.kt +++ b/android/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModel.kt @@ -18,7 +18,7 @@ import net.mullvad.mullvadvpn.applist.ApplicationsProvider import net.mullvad.mullvadvpn.applist.ViewIntent import net.mullvad.mullvadvpn.model.ListItemData import net.mullvad.mullvadvpn.model.WidgetState -import net.mullvad.mullvadvpn.service.SplitTunneling +import net.mullvad.mullvadvpn.ui.serviceconnection.SplitTunneling class SplitTunnelingViewModel( private val appsProvider: ApplicationsProvider, @@ -92,7 +92,7 @@ class SplitTunnelingViewModel( private suspend fun fetchData() { appsProvider.getAppsList() - .partition { app -> splitTunneling.excludedAppList.contains(app.packageName) } + .partition { app -> splitTunneling.isAppExcluded(app.packageName) } .let { (excludedAppsList, notExcludedAppsList) -> // TODO: remove potential package names from splitTunneling list // if they already uninstalled or filtered; but not in ViewModel diff --git a/android/src/test/kotlin/net/mullvad/mullvadvpn/di/AppModuleTest.kt b/android/src/test/kotlin/net/mullvad/mullvadvpn/di/AppModuleTest.kt new file mode 100644 index 0000000000..6984b064c1 --- /dev/null +++ b/android/src/test/kotlin/net/mullvad/mullvadvpn/di/AppModuleTest.kt @@ -0,0 +1,47 @@ +package net.mullvad.mullvadvpn.di + +import android.os.Messenger +import io.mockk.mockk +import io.mockk.unmockkAll +import kotlin.test.assertEquals +import net.mullvad.mullvadvpn.ipc.Event +import net.mullvad.mullvadvpn.ipc.MessageDispatcher +import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnection +import net.mullvad.mullvadvpn.ui.serviceconnection.SplitTunneling +import org.junit.After +import org.junit.Rule +import org.junit.Test +import org.koin.core.parameter.parametersOf +import org.koin.core.qualifier.named +import org.koin.core.scope.Scope +import org.koin.test.KoinTest +import org.koin.test.KoinTestRule + +class AppModuleTest : KoinTest { + + @get:Rule + val koinTestRule = KoinTestRule.create { + modules(appModule) + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun test_scope_linking() { + val appsScope: Scope = getKoin().createScope(APPS_SCOPE, named(APPS_SCOPE)) + val serviceConnectionScope = getKoin().createScope<ServiceConnection>() + + appsScope.linkTo(serviceConnectionScope) + + val mockedMessenger = mockk<Messenger>() + val mockedEventMessageHandler = mockk<MessageDispatcher<Event>>(relaxed = true) + val serviceConnectionSplitTunneling = serviceConnectionScope.get<SplitTunneling>( + parameters = { parametersOf(mockedMessenger, mockedEventMessageHandler) } + ) + + assertEquals(appsScope.get<SplitTunneling>(), serviceConnectionSplitTunneling) + } +} diff --git a/android/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelTest.kt b/android/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelTest.kt index 4bfd98f9da..f2834082c5 100644 --- a/android/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelTest.kt +++ b/android/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelTest.kt @@ -21,7 +21,7 @@ import net.mullvad.mullvadvpn.applist.ViewIntent import net.mullvad.mullvadvpn.assertLists import net.mullvad.mullvadvpn.model.ListItemData import net.mullvad.mullvadvpn.model.WidgetState -import net.mullvad.mullvadvpn.service.SplitTunneling +import net.mullvad.mullvadvpn.ui.serviceconnection.SplitTunneling import org.junit.After import org.junit.Before import org.junit.Rule @@ -80,7 +80,8 @@ class SplitTunnelingViewModelTest { fun test_apps_list_delivered() = runBlockingTest(testCoroutineRule.testDispatcher) { val appExcluded = AppData("test.excluded", 0, "testName1") val appNotExcluded = AppData("test.not.excluded", 0, "testName2") - every { mockedSplitTunneling.excludedAppList } returns listOf(appExcluded.packageName) + every { mockedSplitTunneling.isAppExcluded(appExcluded.packageName) } returns true + every { mockedSplitTunneling.isAppExcluded(appNotExcluded.packageName) } returns false initTestSubject(listOf(appExcluded, appNotExcluded)) testSubject.processIntent(ViewIntent.ViewIsReady) @@ -99,14 +100,15 @@ class SplitTunnelingViewModelTest { assertLists(expectedList, actualList) verifyAll { mockedSplitTunneling.enabled - mockedSplitTunneling.excludedAppList + mockedSplitTunneling.isAppExcluded(appExcluded.packageName) + mockedSplitTunneling.isAppExcluded(appNotExcluded.packageName) } } @Test fun test_remove_app_from_excluded() = runBlockingTest(testCoroutineRule.testDispatcher) { val app = AppData("test", 0, "testName") - every { mockedSplitTunneling.excludedAppList } returns listOf(app.packageName) + every { mockedSplitTunneling.isAppExcluded(app.packageName) } returns true every { mockedSplitTunneling.includeApp(app.packageName) } just Runs initTestSubject(listOf(app)) @@ -137,7 +139,7 @@ class SplitTunnelingViewModelTest { verifyAll { mockedSplitTunneling.enabled - mockedSplitTunneling.excludedAppList + mockedSplitTunneling.isAppExcluded(app.packageName) mockedSplitTunneling.includeApp(app.packageName) } } @@ -145,7 +147,7 @@ class SplitTunnelingViewModelTest { @Test fun test_add_app_to_excluded() = runBlockingTest(testCoroutineRule.testDispatcher) { val app = AppData("test", 0, "testName") - every { mockedSplitTunneling.excludedAppList } returns emptyList() + every { mockedSplitTunneling.isAppExcluded(app.packageName) } returns false every { mockedSplitTunneling.excludeApp(app.packageName) } just Runs initTestSubject(listOf(app)) testSubject.processIntent(ViewIntent.ViewIsReady) @@ -175,7 +177,7 @@ class SplitTunnelingViewModelTest { verifyAll { mockedSplitTunneling.enabled - mockedSplitTunneling.excludedAppList + mockedSplitTunneling.isAppExcluded(app.packageName) mockedSplitTunneling.excludeApp(app.packageName) } } |
