summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAleksandr Granin <aleksandr@mullvad.net>2021-04-08 13:35:29 +0200
committerAleksandr Granin <aleksandr@mullvad.net>2021-04-08 13:35:29 +0200
commiteb0c1ddff764f77c3960b041e63527cfbbe668fb (patch)
tree504d0182bc9f1c431e7bfa77f3fb13f20717c6ce
parent6979590d25be6607fb87954c7dfbfc2b7192868f (diff)
parentdf33a7600b3f3e1976ba2ab5e7b43d4cbedc169a (diff)
downloadmullvadvpn-eb0c1ddff764f77c3960b041e63527cfbbe668fb.tar.xz
mullvadvpn-eb0c1ddff764f77c3960b041e63527cfbbe668fb.zip
Merge branch 'split-split-tunneling'
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/applist/AppListAdapter.kt2
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/applist/AppListItemHolder.kt2
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/di/AppModule.kt18
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/DispatchingHandler.kt4
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Event.kt3
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/MessageDispatcher.kt7
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Request.kt12
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/service/DaemonInstance.kt1
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadVpnService.kt53
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/service/ServiceInstance.kt1
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/service/SplitTunneling.kt69
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/ServiceEndpoint.kt7
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/SplitTunneling.kt56
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/service/persistence/SplitTunnelingPersistence.kt35
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt15
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ui/ServiceDependentFragment.kt2
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnection.kt12
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/SplitTunneling.kt41
-rw-r--r--android/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModel.kt4
-rw-r--r--android/src/test/kotlin/net/mullvad/mullvadvpn/di/AppModuleTest.kt47
-rw-r--r--android/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelTest.kt16
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)
}
}