summaryrefslogtreecommitdiffhomepage
path: root/android/app/src
diff options
context:
space:
mode:
authorAlbin <albin@mullvad.net>2023-07-28 10:03:34 +0200
committerAlbin <albin@mullvad.net>2023-07-28 10:45:57 +0200
commit6e644dfb566b986e7cccbf2a95eac281e4eecf87 (patch)
tree469bc8ce128194512106a91a94bb044853f58907 /android/app/src
parent47bc546a330544b420aab2bdd8c5f91abe7f3161 (diff)
downloadmullvadvpn-6e644dfb566b986e7cccbf2a95eac281e4eecf87.tar.xz
mullvadvpn-6e644dfb566b986e7cccbf2a95eac281e4eecf87.zip
Move vpn service classes to service module
Diffstat (limited to 'android/app/src')
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/VpnServiceModule.kt7
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/DaemonInstance.kt85
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/ForegroundNotificationManager.kt117
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadDaemon.kt295
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadVpnService.kt275
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/AccountCache.kt180
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/AppVersionInfoCache.kt56
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/AuthTokenCache.kt49
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/ConnectionProxy.kt85
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/CustomDns.kt133
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/DaemonDeviceDataSource.kt62
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/LocationInfoCache.kt136
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/RelayListListener.kt108
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/ServiceEndpoint.kt167
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/SettingsListener.kt139
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/SplitTunneling.kt59
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/VoucherRedeemer.kt40
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/VpnPermission.kt45
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/AccountExpiryNotification.kt142
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/NotificationChannel.kt97
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/TunnelStateNotification.kt142
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/TunnelStateNotificationAction.kt58
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/persistence/SplitTunnelingPersistence.kt37
23 files changed, 0 insertions, 2514 deletions
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/VpnServiceModule.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/VpnServiceModule.kt
deleted file mode 100644
index 431023caa2..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/VpnServiceModule.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package net.mullvad.mullvadvpn.di
-
-import androidx.core.app.NotificationManagerCompat
-import org.koin.android.ext.koin.androidContext
-import org.koin.dsl.module
-
-val vpnServiceModule = module { single { NotificationManagerCompat.from(androidContext()) } }
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/DaemonInstance.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/DaemonInstance.kt
deleted file mode 100644
index 4e121bc693..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/DaemonInstance.kt
+++ /dev/null
@@ -1,85 +0,0 @@
-package net.mullvad.mullvadvpn.service
-
-import kotlin.properties.Delegates.observable
-import kotlin.reflect.KClass
-import kotlin.reflect.safeCast
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.channels.ClosedReceiveChannelException
-import kotlinx.coroutines.channels.ReceiveChannel
-import kotlinx.coroutines.channels.actor
-import kotlinx.coroutines.channels.trySendBlocking
-import net.mullvad.mullvadvpn.lib.common.util.Intermittent
-import net.mullvad.mullvadvpn.lib.endpoint.ApiEndpointConfiguration
-
-class DaemonInstance(private val vpnService: MullvadVpnService) {
- sealed class Command {
- data class Start(val apiEndpointConfiguration: ApiEndpointConfiguration) : Command()
- object Stop : Command()
- }
-
- private val commandChannel = spawnActor()
-
- private var daemon by
- observable<MullvadDaemon?>(null) { _, oldInstance, _ -> oldInstance?.onDestroy() }
-
- val intermittentDaemon = Intermittent<MullvadDaemon>()
-
- fun start(apiEndpointConfiguration: ApiEndpointConfiguration) {
- commandChannel.trySendBlocking(Command.Start(apiEndpointConfiguration))
- }
-
- fun stop() {
- commandChannel.trySendBlocking(Command.Stop)
- }
-
- fun onDestroy() {
- commandChannel.close()
- intermittentDaemon.onDestroy()
- }
-
- private fun spawnActor() =
- GlobalScope.actor(Dispatchers.Default, Channel.UNLIMITED) {
- var isRunning = true
-
- while (isRunning) {
- val startCommand = waitForCommand(channel, Command.Start::class) ?: break
- startDaemon(startCommand.apiEndpointConfiguration)
- isRunning = waitForCommand(channel, Command.Stop::class) is Command.Stop
- stopDaemon()
- }
- }
-
- private suspend fun <T : Command> waitForCommand(
- channel: ReceiveChannel<Command>,
- command: KClass<T>
- ): T? {
- return try {
- var receivedCommand: T?
- do {
- receivedCommand = command.safeCast(channel.receive())
- } while (receivedCommand == null)
- receivedCommand
- } catch (exception: ClosedReceiveChannelException) {
- null
- }
- }
-
- private suspend fun startDaemon(apiEndpointConfiguration: ApiEndpointConfiguration) {
- val newDaemon =
- MullvadDaemon(vpnService, apiEndpointConfiguration).apply {
- onDaemonStopped = {
- intermittentDaemon.spawnUpdate(null)
- daemon = null
- }
- }
-
- daemon = newDaemon
- intermittentDaemon.update(newDaemon)
- }
-
- private fun stopDaemon() {
- daemon?.shutdown()
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/ForegroundNotificationManager.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/ForegroundNotificationManager.kt
deleted file mode 100644
index 36d640c719..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/ForegroundNotificationManager.kt
+++ /dev/null
@@ -1,117 +0,0 @@
-package net.mullvad.mullvadvpn.service
-
-import android.app.Service
-import kotlin.properties.Delegates.observable
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.channels.actor
-import kotlinx.coroutines.channels.trySendBlocking
-import kotlinx.coroutines.flow.collect
-import kotlinx.coroutines.flow.onStart
-import net.mullvad.mullvadvpn.lib.common.util.Intermittent
-import net.mullvad.mullvadvpn.lib.common.util.JobTracker
-import net.mullvad.mullvadvpn.model.DeviceState
-import net.mullvad.mullvadvpn.model.TunnelState
-import net.mullvad.mullvadvpn.service.endpoint.ConnectionProxy
-import net.mullvad.mullvadvpn.service.notifications.TunnelStateNotification
-
-class ForegroundNotificationManager(
- val service: MullvadVpnService,
- val connectionProxy: ConnectionProxy,
- val intermittentDaemon: Intermittent<MullvadDaemon>
-) {
- private sealed class UpdaterMessage {
- class UpdateNotification : UpdaterMessage()
- class UpdateAction : UpdaterMessage()
- class NewTunnelState(val newState: TunnelState) : UpdaterMessage()
- }
-
- private val jobTracker = JobTracker()
- private val updater = runUpdater()
-
- private val tunnelStateNotification = TunnelStateNotification(service)
-
- private var loggedIn by
- observable(false) { _, _, _ -> updater.trySendBlocking(UpdaterMessage.UpdateAction()) }
-
- private val tunnelState
- get() = connectionProxy.onStateChange.latestEvent
-
- private val shouldBeOnForeground
- get() = lockedToForeground || !(tunnelState is TunnelState.Disconnected)
-
- var onForeground = false
- private set
-
- var lockedToForeground by
- observable(false) { _, _, _ ->
- updater.trySendBlocking(UpdaterMessage.UpdateNotification())
- }
-
- init {
- connectionProxy.onStateChange.subscribe(this) { newState ->
- updater.trySendBlocking(UpdaterMessage.NewTunnelState(newState))
- }
-
- intermittentDaemon.registerListener(this) { daemon ->
- jobTracker.newBackgroundJob("notificationLoggedInJob") {
- daemon
- ?.deviceStateUpdates
- ?.onStart { emit(daemon.getAndEmitDeviceState()) }
- ?.collect { deviceState -> loggedIn = deviceState is DeviceState.LoggedIn }
- }
- }
-
- updater.trySendBlocking(UpdaterMessage.UpdateNotification())
- }
-
- fun onDestroy() {
- jobTracker.cancelAllJobs()
- intermittentDaemon.unregisterListener(this)
- connectionProxy.onStateChange.unsubscribe(this)
- updater.close()
- }
-
- private fun runUpdater() =
- GlobalScope.actor<UpdaterMessage>(Dispatchers.Main, Channel.UNLIMITED) {
- for (message in channel) {
- when (message) {
- is UpdaterMessage.UpdateNotification -> updateNotification()
- is UpdaterMessage.UpdateAction -> updateNotificationAction()
- is UpdaterMessage.NewTunnelState -> {
- tunnelStateNotification.tunnelState = message.newState
- updateNotification()
- }
- }
- }
- }
-
- fun showOnForeground() {
- service.startForeground(
- TunnelStateNotification.NOTIFICATION_ID,
- tunnelStateNotification.build()
- )
-
- onForeground = true
- }
-
- fun updateNotification() {
- if (shouldBeOnForeground != onForeground) {
- if (shouldBeOnForeground) {
- showOnForeground()
- } else {
- service.stopForeground(Service.STOP_FOREGROUND_DETACH)
- onForeground = false
- }
- }
- }
-
- fun cancelNotification() {
- tunnelStateNotification.visible = false
- }
-
- private fun updateNotificationAction() {
- tunnelStateNotification.showAction = loggedIn
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadDaemon.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadDaemon.kt
deleted file mode 100644
index 089e13ef31..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadDaemon.kt
+++ /dev/null
@@ -1,295 +0,0 @@
-package net.mullvad.mullvadvpn.service
-
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.asSharedFlow
-import net.mullvad.mullvadvpn.lib.endpoint.ApiEndpoint
-import net.mullvad.mullvadvpn.lib.endpoint.ApiEndpointConfiguration
-import net.mullvad.mullvadvpn.model.AppVersionInfo
-import net.mullvad.mullvadvpn.model.Device
-import net.mullvad.mullvadvpn.model.DeviceEvent
-import net.mullvad.mullvadvpn.model.DeviceListEvent
-import net.mullvad.mullvadvpn.model.DeviceState
-import net.mullvad.mullvadvpn.model.DnsOptions
-import net.mullvad.mullvadvpn.model.GeoIpLocation
-import net.mullvad.mullvadvpn.model.GetAccountDataResult
-import net.mullvad.mullvadvpn.model.LoginResult
-import net.mullvad.mullvadvpn.model.ObfuscationSettings
-import net.mullvad.mullvadvpn.model.QuantumResistantState
-import net.mullvad.mullvadvpn.model.RelayList
-import net.mullvad.mullvadvpn.model.RelaySettingsUpdate
-import net.mullvad.mullvadvpn.model.RemoveDeviceEvent
-import net.mullvad.mullvadvpn.model.RemoveDeviceResult
-import net.mullvad.mullvadvpn.model.Settings
-import net.mullvad.mullvadvpn.model.TunnelState
-import net.mullvad.mullvadvpn.model.VoucherSubmissionResult
-import net.mullvad.talpid.util.EventNotifier
-
-class MullvadDaemon(
- vpnService: MullvadVpnService,
- apiEndpointConfiguration: ApiEndpointConfiguration
-) {
- protected var daemonInterfaceAddress = 0L
-
- val onSettingsChange = EventNotifier<Settings?>(null)
- var onTunnelStateChange = EventNotifier<TunnelState>(TunnelState.Disconnected)
-
- var onAppVersionInfoChange: ((AppVersionInfo) -> Unit)? = null
- var onRelayListChange: ((RelayList) -> Unit)? = null
- var onDaemonStopped: (() -> Unit)? = null
-
- private val _deviceStateUpdates = MutableSharedFlow<DeviceState>(extraBufferCapacity = 1)
- val deviceStateUpdates = _deviceStateUpdates.asSharedFlow()
-
- private val _deviceListUpdates = MutableSharedFlow<DeviceListEvent>(extraBufferCapacity = 1)
- val deviceListUpdates = _deviceListUpdates.asSharedFlow()
-
- init {
- System.loadLibrary("mullvad_jni")
-
- initialize(
- vpnService = vpnService,
- cacheDirectory = vpnService.cacheDir.absolutePath,
- resourceDirectory = vpnService.filesDir.absolutePath,
- apiEndpoint = apiEndpointConfiguration.apiEndpoint()
- )
-
- onSettingsChange.notify(getSettings())
-
- onTunnelStateChange.notify(getState() ?: TunnelState.Disconnected)
- }
-
- fun connect() {
- connect(daemonInterfaceAddress)
- }
-
- fun createNewAccount(): String? {
- return createNewAccount(daemonInterfaceAddress)
- }
-
- fun disconnect() {
- disconnect(daemonInterfaceAddress)
- }
-
- fun getAccountData(accountToken: String): GetAccountDataResult {
- return getAccountData(daemonInterfaceAddress, accountToken)
- }
-
- fun getAccountHistory(): String? {
- return getAccountHistory(daemonInterfaceAddress)
- }
-
- fun getWwwAuthToken(): String {
- return getWwwAuthToken(daemonInterfaceAddress) ?: ""
- }
-
- fun getCurrentLocation(): GeoIpLocation? {
- return getCurrentLocation(daemonInterfaceAddress)
- }
-
- fun getCurrentVersion(): String? {
- return getCurrentVersion(daemonInterfaceAddress)
- }
-
- fun getRelayLocations(): RelayList? {
- return getRelayLocations(daemonInterfaceAddress)
- }
-
- fun getSettings(): Settings? {
- return getSettings(daemonInterfaceAddress)
- }
-
- fun getState(): TunnelState? {
- return getState(daemonInterfaceAddress)
- }
-
- fun getVersionInfo(): AppVersionInfo? {
- return getVersionInfo(daemonInterfaceAddress)
- }
-
- fun reconnect() {
- reconnect(daemonInterfaceAddress)
- }
-
- fun clearAccountHistory() {
- clearAccountHistory(daemonInterfaceAddress)
- }
-
- fun loginAccount(accountToken: String): LoginResult {
- return loginAccount(daemonInterfaceAddress, accountToken)
- }
-
- fun logoutAccount() = logoutAccount(daemonInterfaceAddress)
-
- fun getAndEmitDeviceList(accountToken: String): List<Device>? {
- return listDevices(daemonInterfaceAddress, accountToken).also { deviceList ->
- _deviceListUpdates.tryEmit(
- if (deviceList == null) {
- DeviceListEvent.Error
- } else {
- DeviceListEvent.Available(accountToken, deviceList)
- }
- )
- }
- }
-
- fun getAndEmitDeviceState(): DeviceState {
- return getDevice(daemonInterfaceAddress).also { deviceState ->
- _deviceStateUpdates.tryEmit(deviceState)
- }
- }
-
- fun refreshDevice() {
- updateDevice(daemonInterfaceAddress)
- getAndEmitDeviceState()
- }
-
- fun removeDevice(accountToken: String, deviceId: String): RemoveDeviceResult {
- return removeDevice(daemonInterfaceAddress, accountToken, deviceId)
- }
-
- fun setAllowLan(allowLan: Boolean) {
- setAllowLan(daemonInterfaceAddress, allowLan)
- }
-
- fun setAutoConnect(autoConnect: Boolean) {
- setAutoConnect(daemonInterfaceAddress, autoConnect)
- }
-
- fun setDnsOptions(dnsOptions: DnsOptions) {
- setDnsOptions(daemonInterfaceAddress, dnsOptions)
- }
-
- fun setWireguardMtu(wireguardMtu: Int?) {
- setWireguardMtu(daemonInterfaceAddress, wireguardMtu)
- }
-
- fun shutdown() {
- shutdown(daemonInterfaceAddress)
- }
-
- fun submitVoucher(voucher: String): VoucherSubmissionResult {
- return submitVoucher(daemonInterfaceAddress, voucher)
- }
-
- fun updateRelaySettings(update: RelaySettingsUpdate) {
- updateRelaySettings(daemonInterfaceAddress, update)
- }
-
- fun setObfuscationSettings(settings: ObfuscationSettings?) {
- setObfuscationSettings(daemonInterfaceAddress, settings)
- }
-
- fun setQuantumResistant(quantumResistant: QuantumResistantState) {
- setQuantumResistantTunnel(daemonInterfaceAddress, quantumResistant)
- }
-
- fun onDestroy() {
- onSettingsChange.unsubscribeAll()
- onTunnelStateChange.unsubscribeAll()
-
- onAppVersionInfoChange = null
- onRelayListChange = null
- onDaemonStopped = null
-
- deinitialize()
- }
-
- private external fun initialize(
- vpnService: MullvadVpnService,
- cacheDirectory: String,
- resourceDirectory: String,
- apiEndpoint: ApiEndpoint?
- )
-
- private external fun deinitialize()
-
- private external fun connect(daemonInterfaceAddress: Long)
- private external fun createNewAccount(daemonInterfaceAddress: Long): String?
- private external fun disconnect(daemonInterfaceAddress: Long)
- private external fun getAccountData(
- daemonInterfaceAddress: Long,
- accountToken: String
- ): GetAccountDataResult
-
- private external fun getAccountHistory(daemonInterfaceAddress: Long): String?
- private external fun getWwwAuthToken(daemonInterfaceAddress: Long): String?
- private external fun getCurrentLocation(daemonInterfaceAddress: Long): GeoIpLocation?
- private external fun getCurrentVersion(daemonInterfaceAddress: Long): String?
- private external fun getRelayLocations(daemonInterfaceAddress: Long): RelayList?
- private external fun getSettings(daemonInterfaceAddress: Long): Settings?
- private external fun getState(daemonInterfaceAddress: Long): TunnelState?
- private external fun getVersionInfo(daemonInterfaceAddress: Long): AppVersionInfo?
- private external fun reconnect(daemonInterfaceAddress: Long)
- private external fun clearAccountHistory(daemonInterfaceAddress: Long)
- private external fun loginAccount(
- daemonInterfaceAddress: Long,
- accountToken: String?
- ): LoginResult
-
- private external fun logoutAccount(daemonInterfaceAddress: Long)
- private external fun listDevices(
- daemonInterfaceAddress: Long,
- accountToken: String?
- ): List<Device>?
-
- private external fun getDevice(daemonInterfaceAddress: Long): DeviceState
- private external fun updateDevice(daemonInterfaceAddress: Long)
- private external fun removeDevice(
- daemonInterfaceAddress: Long,
- accountToken: String?,
- deviceId: String
- ): RemoveDeviceResult
-
- private external fun setAllowLan(daemonInterfaceAddress: Long, allowLan: Boolean)
- private external fun setAutoConnect(daemonInterfaceAddress: Long, alwaysOn: Boolean)
- private external fun setDnsOptions(daemonInterfaceAddress: Long, dnsOptions: DnsOptions)
- private external fun setWireguardMtu(daemonInterfaceAddress: Long, wireguardMtu: Int?)
- private external fun shutdown(daemonInterfaceAddress: Long)
- private external fun submitVoucher(
- daemonInterfaceAddress: Long,
- voucher: String
- ): VoucherSubmissionResult
-
- private external fun updateRelaySettings(
- daemonInterfaceAddress: Long,
- update: RelaySettingsUpdate
- )
-
- private external fun setObfuscationSettings(
- daemonInterfaceAddress: Long,
- settings: ObfuscationSettings?
- )
-
- private external fun setQuantumResistantTunnel(
- daemonInterfaceAddress: Long,
- quantumResistant: QuantumResistantState
- )
-
- private fun notifyAppVersionInfoEvent(appVersionInfo: AppVersionInfo) {
- onAppVersionInfoChange?.invoke(appVersionInfo)
- }
-
- private fun notifyRelayListEvent(relayList: RelayList) {
- onRelayListChange?.invoke(relayList)
- }
-
- private fun notifySettingsEvent(settings: Settings) {
- onSettingsChange.notify(settings)
- }
-
- private fun notifyTunnelStateEvent(event: TunnelState) {
- onTunnelStateChange.notify(event)
- }
-
- private fun notifyDaemonStopped() {
- onDaemonStopped?.invoke()
- }
-
- private fun notifyDeviceEvent(event: DeviceEvent) {
- _deviceStateUpdates.tryEmit(event.newState)
- }
-
- private fun notifyRemoveDeviceEvent(event: RemoveDeviceEvent) {
- _deviceListUpdates.tryEmit(DeviceListEvent.Available(event.accountToken, event.newDevices))
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadVpnService.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadVpnService.kt
deleted file mode 100644
index 0058b09e4f..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadVpnService.kt
+++ /dev/null
@@ -1,275 +0,0 @@
-package net.mullvad.mullvadvpn.service
-
-import android.app.KeyguardManager
-import android.content.Context
-import android.content.Intent
-import android.net.VpnService
-import android.os.IBinder
-import android.os.Looper
-import android.util.Log
-import kotlin.properties.Delegates.observable
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.launch
-import net.mullvad.mullvadvpn.BuildConfig
-import net.mullvad.mullvadvpn.di.vpnServiceModule
-import net.mullvad.mullvadvpn.lib.endpoint.ApiEndpointConfiguration
-import net.mullvad.mullvadvpn.lib.endpoint.DefaultApiEndpointConfiguration
-import net.mullvad.mullvadvpn.lib.endpoint.getApiEndpointConfigurationExtras
-import net.mullvad.mullvadvpn.model.Settings
-import net.mullvad.mullvadvpn.model.TunnelState
-import net.mullvad.mullvadvpn.service.endpoint.ServiceEndpoint
-import net.mullvad.mullvadvpn.service.notifications.AccountExpiryNotification
-import net.mullvad.mullvadvpn.ui.MainActivity
-import net.mullvad.talpid.TalpidVpnService
-import org.koin.core.context.loadKoinModules
-
-class MullvadVpnService : TalpidVpnService() {
- companion object {
- private val TAG = "mullvad"
-
- val KEY_CONNECT_ACTION = "net.mullvad.mullvadvpn.connect_action"
- val KEY_DISCONNECT_ACTION = "net.mullvad.mullvadvpn.disconnect_action"
- val KEY_QUIT_ACTION = "net.mullvad.mullvadvpn.quit_action"
-
- init {
- System.loadLibrary("mullvad_jni")
- }
- }
-
- private enum class PendingAction {
- Connect,
- Disconnect,
- }
-
- private enum class State {
- Running,
- Stopping,
- Stopped,
- }
-
- private val connectionProxy
- get() = endpoint.connectionProxy
-
- private var state = State.Running
-
- private var setUpDaemonJob: Job? = null
-
- private lateinit var accountExpiryNotification: AccountExpiryNotification
- private lateinit var daemonInstance: DaemonInstance
- private lateinit var endpoint: ServiceEndpoint
- private lateinit var keyguardManager: KeyguardManager
- private lateinit var notificationManager: ForegroundNotificationManager
-
- private var pendingAction by
- observable<PendingAction?>(null) { _, _, _ ->
- endpoint.settingsListener.settings?.let { settings -> handlePendingAction(settings) }
- }
-
- private var apiEndpointConfiguration: ApiEndpointConfiguration =
- DefaultApiEndpointConfiguration()
-
- override fun onCreate() {
- super.onCreate()
- Log.d(TAG, "Initializing service")
-
- loadKoinModules(vpnServiceModule)
-
- daemonInstance = DaemonInstance(this)
- keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
-
- endpoint =
- ServiceEndpoint(
- Looper.getMainLooper(),
- daemonInstance.intermittentDaemon,
- connectivityListener,
- this
- )
-
- endpoint.splitTunneling.onChange.subscribe(this@MullvadVpnService) { excludedApps ->
- disallowedApps = excludedApps
- markTunAsStale()
- connectionProxy.reconnect()
- }
-
- notificationManager =
- ForegroundNotificationManager(this, connectionProxy, daemonInstance.intermittentDaemon)
-
- accountExpiryNotification =
- AccountExpiryNotification(
- this,
- daemonInstance.intermittentDaemon,
- endpoint.accountCache
- )
-
- // Remove any leftover tunnel state persistence data
- getSharedPreferences("tunnel_state", MODE_PRIVATE).edit().clear().commit()
- }
-
- override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
- Log.d(TAG, "Starting service")
-
- if (BuildConfig.DEBUG) {
- intent?.getApiEndpointConfigurationExtras()?.let { apiEndpointConfiguration = it }
- }
-
- daemonInstance.apply {
- intermittentDaemon.registerListener(this@MullvadVpnService) { daemon ->
- handleDaemonInstance(daemon)
- }
-
- start(apiEndpointConfiguration)
- }
-
- val startResult = super.onStartCommand(intent, flags, startId)
- var quitCommand = false
-
- // Always promote to foreground if connect/disconnect actions are provided to mitigate cases
- // where the service would potentially otherwise be too slow running `startForeground`.
- if (intent?.action == KEY_CONNECT_ACTION || intent?.action == KEY_DISCONNECT_ACTION) {
- notificationManager.showOnForeground()
- }
-
- notificationManager.updateNotification()
-
- if (!keyguardManager.isDeviceLocked) {
- val action = intent?.action
-
- if (action == VpnService.SERVICE_INTERFACE || action == KEY_CONNECT_ACTION) {
- pendingAction = PendingAction.Connect
- } else if (action == KEY_DISCONNECT_ACTION) {
- pendingAction = PendingAction.Disconnect
- } else if (action == KEY_QUIT_ACTION && !notificationManager.onForeground) {
- quitCommand = true
- stop()
- }
- }
-
- if (state == State.Stopping && !quitCommand) {
- restart()
- }
-
- return startResult
- }
-
- override fun onBind(intent: Intent): IBinder {
- Log.d(TAG, "New connection to service")
- return super.onBind(intent) ?: endpoint.messenger.binder
- }
-
- override fun onRebind(intent: Intent) {
- Log.d(TAG, "Connection to service restored")
- if (state == State.Stopping) {
- restart()
- }
- }
-
- override fun onRevoke() {
- pendingAction = PendingAction.Disconnect
- }
-
- override fun onUnbind(intent: Intent): Boolean {
- Log.d(TAG, "Closed all connections to service")
-
- if (state != State.Running) {
- stop()
- }
-
- return true
- }
-
- override fun onDestroy() {
- Log.d(TAG, "Service has stopped")
- state = State.Stopped
- accountExpiryNotification.onDestroy()
- notificationManager.onDestroy()
- daemonInstance.onDestroy()
- super.onDestroy()
- }
-
- override fun onTaskRemoved(rootIntent: Intent?) {
- connectionProxy.onStateChange.latestEvent.let { tunnelState ->
- Log.d(TAG, "Task removed (tunnelState=$tunnelState)")
- if (tunnelState == TunnelState.Disconnected) {
- notificationManager.cancelNotification()
- stop()
- }
- }
- }
-
- private fun handleDaemonInstance(daemon: MullvadDaemon?) {
- setUpDaemonJob?.cancel()
-
- if (daemon != null) {
- setUpDaemonJob = setUpDaemon(daemon)
- } else {
- Log.d(TAG, "Daemon has stopped")
-
- if (state == State.Running) {
- restart()
- }
- }
- }
-
- private fun setUpDaemon(daemon: MullvadDaemon) =
- GlobalScope.launch(Dispatchers.Main) {
- if (state != State.Stopped) {
- val settings = daemon.getSettings()
-
- if (settings != null) {
- handlePendingAction(settings)
- } else {
- restart()
- }
- }
- }
-
- private fun stop() {
- Log.d(TAG, "Stopping service")
- state = State.Stopping
- daemonInstance.stop()
- stopSelf()
- }
-
- private fun restart() {
- if (state != State.Stopped) {
- Log.d(TAG, "Restarting service")
-
- state = State.Running
-
- daemonInstance.apply {
- stop()
- start(apiEndpointConfiguration)
- }
- } else {
- Log.d(TAG, "Ignoring restart because onDestroy has executed")
- }
- }
-
- private fun handlePendingAction(settings: Settings) {
- when (pendingAction) {
- PendingAction.Connect -> {
- if (settings != null) {
- connectionProxy.connect()
- } else {
- openUi()
- }
- }
- PendingAction.Disconnect -> connectionProxy.disconnect()
- null -> return
- }
-
- pendingAction = null
- }
-
- private fun openUi() {
- val intent =
- Intent(this, MainActivity::class.java).apply {
- addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
- }
-
- startActivity(intent)
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/AccountCache.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/AccountCache.kt
deleted file mode 100644
index ad8b96f9a5..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/AccountCache.kt
+++ /dev/null
@@ -1,180 +0,0 @@
-package net.mullvad.mullvadvpn.service.endpoint
-
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.channels.ClosedReceiveChannelException
-import kotlinx.coroutines.channels.actor
-import kotlinx.coroutines.channels.trySendBlocking
-import net.mullvad.mullvadvpn.lib.common.util.JobTracker
-import net.mullvad.mullvadvpn.lib.common.util.parseAsDateTime
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.lib.ipc.Request
-import net.mullvad.mullvadvpn.model.AccountCreationResult
-import net.mullvad.mullvadvpn.model.AccountExpiry
-import net.mullvad.mullvadvpn.model.AccountHistory
-import net.mullvad.mullvadvpn.model.GetAccountDataResult
-import net.mullvad.talpid.util.EventNotifier
-
-class AccountCache(private val endpoint: ServiceEndpoint) {
- companion object {
- private sealed class Command {
- object CreateAccount : Command()
- data class Login(val account: String) : Command()
- object Logout : Command()
- }
- }
-
- private val commandChannel = spawnActor()
-
- private val daemon
- get() = endpoint.intermittentDaemon
-
- val onAccountExpiryChange = EventNotifier<AccountExpiry>(AccountExpiry.Missing)
- val onAccountHistoryChange = EventNotifier<AccountHistory>(AccountHistory.Missing)
-
- private val jobTracker = JobTracker()
-
- private var accountExpiry by onAccountExpiryChange.notifiable()
- private var accountHistory by onAccountHistoryChange.notifiable()
-
- private var cachedAccountToken: String? = null
- private var cachedCreatedAccountToken: String? = null
-
- val isNewAccount: Boolean
- get() = cachedAccountToken == cachedCreatedAccountToken
-
- init {
- jobTracker.newBackgroundJob("autoFetchAccountExpiry") {
- daemon.await().deviceStateUpdates.collect { deviceState ->
- accountExpiry =
- deviceState
- .token()
- .also { cachedAccountToken = it }
- ?.let { fetchAccountExpiry(it) }
- ?: AccountExpiry.Missing
- }
- }
-
- onAccountHistoryChange.subscribe(this) { history ->
- endpoint.sendEvent(Event.AccountHistoryEvent(history))
- }
-
- onAccountExpiryChange.subscribe(this) { endpoint.sendEvent(Event.AccountExpiryEvent(it)) }
-
- endpoint.dispatcher.apply {
- registerHandler(Request.CreateAccount::class) { _ ->
- commandChannel.trySendBlocking(Command.CreateAccount)
- }
-
- registerHandler(Request.Login::class) { request ->
- request.account?.let { account ->
- commandChannel.trySendBlocking(Command.Login(account))
- }
- }
-
- registerHandler(Request.Logout::class) { _ ->
- commandChannel.trySendBlocking(Command.Logout)
- }
-
- registerHandler(Request.FetchAccountExpiry::class) { _ ->
- jobTracker.newBackgroundJob("fetchAccountExpiry") {
- accountExpiry =
- cachedAccountToken?.let { fetchAccountExpiry(it) } ?: AccountExpiry.Missing
- }
- }
-
- registerHandler(Request.FetchAccountHistory::class) { _ ->
- jobTracker.newBackgroundJob("fetchAccountHistory") {
- accountHistory = fetchAccountHistory()
- }
- }
-
- registerHandler(Request.ClearAccountHistory::class) { _ ->
- jobTracker.newBackgroundJob("clearAccountHistory") { clearAccountHistory() }
- }
- }
- }
-
- fun onDestroy() {
- jobTracker.cancelAllJobs()
-
- onAccountExpiryChange.unsubscribeAll()
- onAccountHistoryChange.unsubscribeAll()
-
- commandChannel.close()
- }
-
- private fun spawnActor() =
- GlobalScope.actor<Command>(Dispatchers.Default, Channel.UNLIMITED) {
- try {
- for (command in channel) {
- when (command) {
- is Command.CreateAccount -> doCreateAccount()
- is Command.Login -> doLogin(command.account)
- is Command.Logout -> doLogout()
- }
- }
- } catch (exception: ClosedReceiveChannelException) {
- // Command channel was closed, stop the actor
- }
- }
-
- private suspend fun clearAccountHistory() {
- daemon.await().clearAccountHistory()
- accountHistory = fetchAccountHistory()
- }
-
- private suspend fun doCreateAccount() {
- daemon
- .await()
- .createNewAccount()
- .also { newAccountToken -> cachedCreatedAccountToken = newAccountToken }
- .let { newAccountToken ->
- if (newAccountToken != null) {
- AccountCreationResult.Success(newAccountToken)
- } else {
- AccountCreationResult.Failure
- }
- }
- .also { result -> endpoint.sendEvent(Event.AccountCreationEvent(result)) }
- }
-
- private suspend fun doLogin(account: String) {
- daemon.await().loginAccount(account).also { result ->
- endpoint.sendEvent(Event.LoginEvent(result))
- }
- }
-
- private suspend fun doLogout() {
- daemon.await().logoutAccount()
- accountHistory = fetchAccountHistory()
- }
-
- private suspend fun fetchAccountHistory(): AccountHistory {
- return daemon.await().getAccountHistory().let { history ->
- if (history != null) {
- AccountHistory.Available(history)
- } else {
- AccountHistory.Missing
- }
- }
- }
-
- private suspend fun fetchAccountExpiry(accountToken: String): AccountExpiry {
- return fetchAccountData(accountToken).let { result ->
- if (result is GetAccountDataResult.Ok) {
- result.accountData.expiry.parseAsDateTime()?.let { parsedDateTime ->
- AccountExpiry.Available(parsedDateTime)
- }
- ?: AccountExpiry.Missing
- } else {
- AccountExpiry.Missing
- }
- }
- }
-
- private suspend fun fetchAccountData(accountToken: String): GetAccountDataResult {
- return daemon.await().getAccountData(accountToken)
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/AppVersionInfoCache.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/AppVersionInfoCache.kt
deleted file mode 100644
index 767ac3e251..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/AppVersionInfoCache.kt
+++ /dev/null
@@ -1,56 +0,0 @@
-package net.mullvad.mullvadvpn.service.endpoint
-
-import kotlin.properties.Delegates.observable
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.model.AppVersionInfo
-import net.mullvad.mullvadvpn.service.MullvadDaemon
-
-class AppVersionInfoCache(endpoint: ServiceEndpoint) {
- private val daemon = endpoint.intermittentDaemon
-
- var appVersionInfo by
- observable<AppVersionInfo?>(null) { _, _, info ->
- endpoint.sendEvent(Event.AppVersionInfo(info))
- }
- private set
-
- var currentVersion by
- observable<String?>(null) { _, _, version ->
- endpoint.sendEvent(Event.CurrentVersion(version))
- }
- private set
-
- init {
- daemon.registerListener(this) { newDaemon ->
- newDaemon?.let { daemon ->
- initializeCurrentVersion(daemon)
- registerVersionInfoListener(daemon)
- fetchInitialVersionInfo(daemon)
- }
- }
- }
-
- fun onDestroy() {
- daemon.unregisterListener(this)
- }
-
- private fun initializeCurrentVersion(daemon: MullvadDaemon) {
- if (currentVersion == null) {
- currentVersion = daemon.getCurrentVersion()
- }
- }
-
- private fun registerVersionInfoListener(daemon: MullvadDaemon) {
- daemon.onAppVersionInfoChange = { newAppVersionInfo ->
- synchronized(this@AppVersionInfoCache) { appVersionInfo = newAppVersionInfo }
- }
- }
-
- private fun fetchInitialVersionInfo(daemon: MullvadDaemon) {
- synchronized(this@AppVersionInfoCache) {
- if (appVersionInfo == null) {
- appVersionInfo = daemon.getVersionInfo()
- }
- }
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/AuthTokenCache.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/AuthTokenCache.kt
deleted file mode 100644
index 6506c0469d..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/AuthTokenCache.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-package net.mullvad.mullvadvpn.service.endpoint
-
-import kotlin.properties.Delegates.observable
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.channels.ClosedReceiveChannelException
-import kotlinx.coroutines.channels.actor
-import kotlinx.coroutines.channels.trySendBlocking
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.lib.ipc.Request
-
-class AuthTokenCache(endpoint: ServiceEndpoint) {
- companion object {
- private enum class Command {
- Fetch
- }
- }
-
- private val daemon = endpoint.intermittentDaemon
- private val requestQueue = spawnActor()
-
- var authToken by
- observable<String?>(null) { _, _, token -> endpoint.sendEvent(Event.AuthToken(token)) }
- private set
-
- init {
- endpoint.dispatcher.registerHandler(Request.FetchAuthToken::class) { _ ->
- requestQueue.trySendBlocking(Command.Fetch)
- }
- }
-
- fun onDestroy() {
- requestQueue.close()
- }
-
- private fun spawnActor() =
- GlobalScope.actor<Command>(Dispatchers.Default, Channel.UNLIMITED) {
- try {
- for (command in channel) {
- when (command) {
- Command.Fetch -> authToken = daemon.await().getWwwAuthToken()
- }
- }
- } catch (exception: ClosedReceiveChannelException) {
- // Closed sender, so stop the actor
- }
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/ConnectionProxy.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/ConnectionProxy.kt
deleted file mode 100644
index a2c97a05bd..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/ConnectionProxy.kt
+++ /dev/null
@@ -1,85 +0,0 @@
-package net.mullvad.mullvadvpn.service.endpoint
-
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.channels.ClosedReceiveChannelException
-import kotlinx.coroutines.channels.actor
-import kotlinx.coroutines.channels.trySendBlocking
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.lib.ipc.Request
-import net.mullvad.mullvadvpn.model.TunnelState
-import net.mullvad.talpid.util.EventNotifier
-
-class ConnectionProxy(val vpnPermission: VpnPermission, endpoint: ServiceEndpoint) {
- private enum class Command {
- CONNECT,
- RECONNECT,
- DISCONNECT,
- }
-
- private val commandChannel = spawnActor()
- private val daemon = endpoint.intermittentDaemon
- private val initialState = TunnelState.Disconnected
-
- var onStateChange = EventNotifier<TunnelState>(initialState)
-
- var state by onStateChange.notifiable()
- private set
-
- init {
- daemon.registerListener(this) { newDaemon ->
- newDaemon?.onTunnelStateChange?.subscribe(this@ConnectionProxy) { newState ->
- state = newState
- }
- }
-
- onStateChange.subscribe(this) { tunnelState ->
- endpoint.sendEvent(Event.TunnelStateChange(tunnelState))
- }
-
- endpoint.dispatcher.apply {
- registerHandler(Request.Connect::class) { _ -> connect() }
- registerHandler(Request.Reconnect::class) { _ -> reconnect() }
- registerHandler(Request.Disconnect::class) { _ -> disconnect() }
- }
- }
-
- fun connect() {
- commandChannel.trySendBlocking(Command.CONNECT)
- }
-
- fun reconnect() {
- commandChannel.trySendBlocking(Command.RECONNECT)
- }
-
- fun disconnect() {
- commandChannel.trySendBlocking(Command.DISCONNECT)
- }
-
- fun onDestroy() {
- commandChannel.close()
- onStateChange.unsubscribeAll()
- daemon.unregisterListener(this)
- }
-
- private fun spawnActor() =
- GlobalScope.actor<Command>(Dispatchers.Default, Channel.UNLIMITED) {
- try {
- while (true) {
- val command = channel.receive()
-
- when (command) {
- Command.CONNECT -> {
- vpnPermission.request()
- daemon.await().connect()
- }
- Command.RECONNECT -> daemon.await().reconnect()
- Command.DISCONNECT -> daemon.await().disconnect()
- }
- }
- } catch (exception: ClosedReceiveChannelException) {
- // Closed sender, so stop the actor
- }
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/CustomDns.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/CustomDns.kt
deleted file mode 100644
index fe8f55a66d..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/CustomDns.kt
+++ /dev/null
@@ -1,133 +0,0 @@
-package net.mullvad.mullvadvpn.service.endpoint
-
-import java.net.InetAddress
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.channels.ClosedReceiveChannelException
-import kotlinx.coroutines.channels.actor
-import kotlinx.coroutines.channels.trySendBlocking
-import net.mullvad.mullvadvpn.lib.ipc.Request
-import net.mullvad.mullvadvpn.model.CustomDnsOptions
-import net.mullvad.mullvadvpn.model.DefaultDnsOptions
-import net.mullvad.mullvadvpn.model.DnsOptions
-import net.mullvad.mullvadvpn.model.DnsState
-
-class CustomDns(private val endpoint: ServiceEndpoint) {
- private sealed class Command {
- @Deprecated("Use SetDnsOptions") class AddDnsServer(val server: InetAddress) : Command()
- @Deprecated("Use SetDnsOptions") class RemoveDnsServer(val server: InetAddress) : Command()
- @Deprecated("Use SetDnsOptions")
- class ReplaceDnsServer(val oldServer: InetAddress, val newServer: InetAddress) : Command()
- @Deprecated("Use SetDnsOptions") class SetEnabled(val enabled: Boolean) : Command()
-
- class SetDnsOptions(val dnsOptions: DnsOptions) : Command()
- }
-
- private val commandChannel = spawnActor()
- private val dnsServers = ArrayList<InetAddress>()
-
- private val daemon
- get() = endpoint.intermittentDaemon
-
- private var enabled = false
-
- init {
- endpoint.settingsListener.dnsOptionsNotifier.subscribe(this) { maybeDnsOptions ->
- maybeDnsOptions?.let { dnsOptions ->
- enabled = dnsOptions.state == DnsState.Custom
- dnsServers.clear()
- dnsServers.addAll(dnsOptions.customOptions.addresses)
- }
- }
-
- endpoint.dispatcher.apply {
- registerHandler(Request.AddCustomDnsServer::class) { request ->
- commandChannel.trySendBlocking(Command.AddDnsServer(request.address))
- }
-
- registerHandler(Request.RemoveCustomDnsServer::class) { request ->
- commandChannel.trySendBlocking(Command.RemoveDnsServer(request.address))
- }
-
- registerHandler(Request.ReplaceCustomDnsServer::class) { request ->
- commandChannel.trySendBlocking(
- Command.ReplaceDnsServer(request.oldAddress, request.newAddress)
- )
- }
-
- registerHandler(Request.SetEnableCustomDns::class) { request ->
- commandChannel.trySendBlocking(Command.SetEnabled(request.enable))
- }
-
- registerHandler(Request.SetDnsOptions::class) { request ->
- commandChannel.trySendBlocking(Command.SetDnsOptions(request.dnsOptions))
- }
- }
- }
-
- fun onDestroy() {
- endpoint.settingsListener.dnsOptionsNotifier.unsubscribe(this)
- commandChannel.close()
- }
-
- private fun spawnActor() =
- GlobalScope.actor<Command>(Dispatchers.Default, Channel.UNLIMITED) {
- try {
- while (true) {
- val command = channel.receive()
-
- when (command) {
- is Command.AddDnsServer -> doAddDnsServer(command.server)
- is Command.RemoveDnsServer -> doRemoveDnsServer(command.server)
- is Command.ReplaceDnsServer -> {
- doReplaceDnsServer(command.oldServer, command.newServer)
- }
- is Command.SetEnabled -> changeDnsOptions(command.enabled)
- is Command.SetDnsOptions -> setDnsOptions(command.dnsOptions)
- }
- }
- } catch (exception: ClosedReceiveChannelException) {
- // Closed sender, so stop the actor
- }
- }
-
- private suspend fun doAddDnsServer(server: InetAddress) {
- if (!dnsServers.contains(server)) {
- dnsServers.add(server)
- changeDnsOptions(enabled)
- }
- }
-
- private suspend fun doReplaceDnsServer(oldServer: InetAddress, newServer: InetAddress) {
- if (oldServer != newServer && !dnsServers.contains(newServer)) {
- val index = dnsServers.indexOf(oldServer)
-
- if (index >= 0) {
- dnsServers.removeAt(index)
- dnsServers.add(index, newServer)
- changeDnsOptions(enabled)
- }
- }
- }
-
- private suspend fun doRemoveDnsServer(server: InetAddress) {
- if (dnsServers.remove(server)) {
- changeDnsOptions(enabled)
- }
- }
-
- private suspend fun changeDnsOptions(enable: Boolean) {
- val options =
- DnsOptions(
- state = if (enable) DnsState.Custom else DnsState.Default,
- customOptions = CustomDnsOptions(dnsServers),
- defaultOptions = DefaultDnsOptions()
- )
- daemon.await().setDnsOptions(options)
- }
-
- private suspend fun setDnsOptions(dnsOptions: DnsOptions) {
- daemon.await().setDnsOptions(dnsOptions)
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/DaemonDeviceDataSource.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/DaemonDeviceDataSource.kt
deleted file mode 100644
index db264ed1fe..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/DaemonDeviceDataSource.kt
+++ /dev/null
@@ -1,62 +0,0 @@
-package net.mullvad.mullvadvpn.service.endpoint
-
-import kotlinx.coroutines.flow.collect
-import net.mullvad.mullvadvpn.lib.common.util.JobTracker
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.lib.ipc.Request
-import net.mullvad.mullvadvpn.service.MullvadDaemon
-
-class DaemonDeviceDataSource(val endpoint: ServiceEndpoint) {
- private val tracker = JobTracker()
-
- init {
- endpoint.intermittentDaemon.registerListener(this) { daemon ->
- if (daemon != null) {
- launchDeviceEndpointJobs(daemon)
- } else {
- tracker.cancelAllJobs()
- }
- }
- }
-
- private fun launchDeviceEndpointJobs(daemon: MullvadDaemon) {
- tracker.newBackgroundJob("propagateDeviceUpdatesJob") {
- daemon.deviceStateUpdates.collect { newState ->
- endpoint.sendEvent(Event.DeviceStateEvent(newState))
- }
- }
-
- tracker.newBackgroundJob("propagateDeviceListUpdatesJob") {
- daemon.deviceListUpdates.collect { newState ->
- endpoint.sendEvent(Event.DeviceListUpdate(newState))
- }
- }
-
- endpoint.dispatcher.registerHandler(Request.GetDevice::class) {
- tracker.newBackgroundJob("getDeviceJob") { daemon.getAndEmitDeviceState() }
- }
-
- endpoint.dispatcher.registerHandler(Request.RefreshDeviceState::class) {
- tracker.newBackgroundJob("refreshDeviceJob") { daemon.refreshDevice() }
- }
-
- endpoint.dispatcher.registerHandler(Request.RemoveDevice::class) { request ->
- tracker.newBackgroundJob("removeDeviceJob") {
- daemon.removeDevice(request.accountToken, request.deviceId).also { result ->
- endpoint.sendEvent(Event.DeviceRemovalEvent(request.deviceId, result))
- }
- }
- }
-
- endpoint.dispatcher.registerHandler(Request.GetDeviceList::class) { request ->
- tracker.newBackgroundJob("getDeviceListJob") {
- daemon.getAndEmitDeviceList(request.accountToken)
- }
- }
- }
-
- fun onDestroy() {
- tracker.cancelAllJobs()
- endpoint.intermittentDaemon.unregisterListener(this)
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/LocationInfoCache.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/LocationInfoCache.kt
deleted file mode 100644
index 68d6b56f6e..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/LocationInfoCache.kt
+++ /dev/null
@@ -1,136 +0,0 @@
-package net.mullvad.mullvadvpn.service.endpoint
-
-import kotlin.properties.Delegates.observable
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.channels.ClosedReceiveChannelException
-import kotlinx.coroutines.channels.ReceiveChannel
-import kotlinx.coroutines.channels.actor
-import kotlinx.coroutines.channels.trySendBlocking
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.flow.receiveAsFlow
-import net.mullvad.mullvadvpn.lib.common.util.toGeographicLocationConstraint
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.model.Constraint
-import net.mullvad.mullvadvpn.model.GeoIpLocation
-import net.mullvad.mullvadvpn.model.RelaySettings
-import net.mullvad.mullvadvpn.model.TunnelState
-import net.mullvad.mullvadvpn.util.ExponentialBackoff
-import net.mullvad.talpid.tunnel.ActionAfterDisconnect
-
-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.trySendBlocking(RequestFetch.ForRealLocation)
- }
- is TunnelState.Connecting -> location = newState.location
- is TunnelState.Connected -> {
- location = newState.location
- fetchRequestChannel.trySendBlocking(RequestFetch.ForRelayLocation)
- }
- is TunnelState.Disconnecting -> {
- when (newState.actionAfterDisconnect) {
- ActionAfterDisconnect.Nothing -> location = lastKnownRealLocation
- ActionAfterDisconnect.Block -> location = null
- ActionAfterDisconnect.Reconnect -> location = selectedRelayLocation
- }
- }
- is TunnelState.Error -> location = null
- }
- }
-
- init {
- endpoint.connectionProxy.onStateChange.subscribe(this) { newState -> state = newState }
-
- endpoint.connectivityListener.connectivityNotifier.subscribe(this) { isConnected ->
- if (isConnected && state is TunnelState.Disconnected) {
- fetchRequestChannel.trySendBlocking(RequestFetch.ForRealLocation)
- }
- }
-
- endpoint.settingsListener.relaySettingsNotifier.subscribe(this, ::updateSelectedLocation)
- }
-
- fun onDestroy() {
- endpoint.connectionProxy.onStateChange.unsubscribe(this)
- endpoint.connectivityListener.connectivityNotifier.unsubscribe(this)
- endpoint.settingsListener.relaySettingsNotifier.unsubscribe(this)
-
- fetchRequestChannel.close()
- }
-
- private fun runFetcher() =
- GlobalScope.actor<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?.toGeographicLocationConstraint()?.location
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/RelayListListener.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/RelayListListener.kt
deleted file mode 100644
index 1abf64907c..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/RelayListListener.kt
+++ /dev/null
@@ -1,108 +0,0 @@
-package net.mullvad.mullvadvpn.service.endpoint
-
-import kotlin.properties.Delegates.observable
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.channels.ClosedReceiveChannelException
-import kotlinx.coroutines.channels.actor
-import kotlinx.coroutines.channels.trySendBlocking
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.lib.ipc.Request
-import net.mullvad.mullvadvpn.model.Constraint
-import net.mullvad.mullvadvpn.model.GeographicLocationConstraint
-import net.mullvad.mullvadvpn.model.LocationConstraint
-import net.mullvad.mullvadvpn.model.RelayConstraintsUpdate
-import net.mullvad.mullvadvpn.model.RelayList
-import net.mullvad.mullvadvpn.model.RelaySettingsUpdate
-import net.mullvad.mullvadvpn.model.WireguardConstraints
-import net.mullvad.mullvadvpn.service.MullvadDaemon
-
-class RelayListListener(endpoint: ServiceEndpoint) {
- companion object {
- private enum class Command {
- SetRelayLocation,
- SetWireguardConstraints
- }
- }
-
- private val commandChannel = spawnActor()
- private val daemon = endpoint.intermittentDaemon
-
- private var selectedRelayLocation by
- observable<GeographicLocationConstraint?>(null) { _, _, _ ->
- commandChannel.trySendBlocking(Command.SetRelayLocation)
- }
- private var selectedWireguardConstraints by
- observable<WireguardConstraints?>(null) { _, _, _ ->
- commandChannel.trySendBlocking(Command.SetWireguardConstraints)
- }
-
- var relayList by
- observable<RelayList?>(null) { _, _, relays ->
- endpoint.sendEvent(Event.NewRelayList(relays))
- }
- private set
-
- init {
- daemon.registerListener(this) { newDaemon ->
- newDaemon?.let { daemon ->
- setUpListener(daemon)
- fetchInitialRelayList(daemon)
- }
- }
-
- endpoint.dispatcher.registerHandler(Request.SetRelayLocation::class) { request ->
- selectedRelayLocation = request.relayLocation
- }
-
- endpoint.dispatcher.registerHandler(Request.SetWireguardConstraints::class) { request ->
- selectedWireguardConstraints = request.wireguardConstraints
- }
- }
-
- fun onDestroy() {
- commandChannel.close()
- daemon.unregisterListener(this)
- }
-
- private fun setUpListener(daemon: MullvadDaemon) {
- daemon.onRelayListChange = { relayLocations -> relayList = relayLocations }
- }
-
- private fun fetchInitialRelayList(daemon: MullvadDaemon) {
- synchronized(this) {
- if (relayList == null) {
- relayList = daemon.getRelayLocations()
- }
- }
- }
-
- private fun spawnActor() =
- GlobalScope.actor<Command>(Dispatchers.Default, Channel.CONFLATED) {
- try {
- for (command in channel) {
- when (command) {
- Command.SetRelayLocation,
- Command.SetWireguardConstraints -> updateRelayConstraints()
- }
- }
- } catch (exception: ClosedReceiveChannelException) {
- // Closed sender, so stop the actor
- }
- }
-
- private suspend fun updateRelayConstraints() {
- val location: Constraint<LocationConstraint> =
- selectedRelayLocation?.let { location ->
- Constraint.Only(LocationConstraint.Location(location))
- }
- ?: Constraint.Any()
- val wireguardConstraints: WireguardConstraints? = selectedWireguardConstraints
-
- val update =
- RelaySettingsUpdate.Normal(RelayConstraintsUpdate(location, wireguardConstraints))
-
- daemon.await().updateRelaySettings(update)
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/ServiceEndpoint.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/ServiceEndpoint.kt
deleted file mode 100644
index 1d6cb9f9a7..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/ServiceEndpoint.kt
+++ /dev/null
@@ -1,167 +0,0 @@
-package net.mullvad.mullvadvpn.service.endpoint
-
-import android.content.Context
-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.trySendBlocking
-import net.mullvad.mullvadvpn.lib.common.util.Intermittent
-import net.mullvad.mullvadvpn.lib.ipc.DispatchingHandler
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.lib.ipc.Request
-import net.mullvad.mullvadvpn.lib.ipc.extensions.trySendEvent
-import net.mullvad.mullvadvpn.service.MullvadDaemon
-import net.mullvad.mullvadvpn.service.persistence.SplitTunnelingPersistence
-import net.mullvad.talpid.ConnectivityListener
-
-const val SHOULD_LOG_DEAD_OBJECT_EXCEPTION = true
-
-class ServiceEndpoint(
- looper: Looper,
- internal val intermittentDaemon: Intermittent<MullvadDaemon>,
- val connectivityListener: ConnectivityListener,
- context: Context
-) {
- companion object {
- sealed class Command {
- data class RegisterListener(val listener: Messenger) : Command()
- data class UnregisterListener(val listenerId: Int) : Command()
- }
- }
-
- private val listeners = mutableMapOf<Int, Messenger>()
- private val commands: SendChannel<Command> = startRegistrator()
-
- internal val dispatcher = DispatchingHandler(looper) { message -> Request.fromMessage(message) }
-
- private var listenerIdCounter = 0
-
- val messenger = Messenger(dispatcher)
-
- val vpnPermission = VpnPermission(context, this)
-
- val connectionProxy = ConnectionProxy(vpnPermission, this)
- val settingsListener = SettingsListener(this)
-
- val accountCache = AccountCache(this)
- val appVersionInfoCache = AppVersionInfoCache(this)
- val authTokenCache = AuthTokenCache(this)
- val customDns = CustomDns(this)
- val locationInfoCache = LocationInfoCache(this)
- val relayListListener = RelayListListener(this)
- val splitTunneling = SplitTunneling(SplitTunnelingPersistence(context), this)
- val voucherRedeemer = VoucherRedeemer(this)
-
- private val deviceRepositoryBackend = DaemonDeviceDataSource(this)
-
- init {
- dispatcher.apply {
- registerHandler(Request.RegisterListener::class) { request ->
- commands.trySendBlocking(Command.RegisterListener(request.listener))
- }
-
- registerHandler(Request.UnregisterListener::class) { request ->
- commands.trySendBlocking(Command.UnregisterListener(request.listenerId))
- }
- }
- }
-
- fun onDestroy() {
- dispatcher.onDestroy()
- commands.close()
-
- accountCache.onDestroy()
- appVersionInfoCache.onDestroy()
- authTokenCache.onDestroy()
- connectionProxy.onDestroy()
- customDns.onDestroy()
- deviceRepositoryBackend.onDestroy()
- locationInfoCache.onDestroy()
- relayListListener.onDestroy()
- settingsListener.onDestroy()
- splitTunneling.onDestroy()
- voucherRedeemer.onDestroy()
- }
-
- internal fun sendEvent(event: Event) {
- synchronized(this) {
- val deadListeners = mutableSetOf<Int>()
-
- for ((id, listener) in listeners) {
- if (!listener.trySendEvent(event, SHOULD_LOG_DEAD_OBJECT_EXCEPTION)) {
- deadListeners.add(id)
- }
- }
- deadListeners.forEach { listeners.remove(it) }
- }
- }
-
- private fun startRegistrator() =
- GlobalScope.actor<Command>(Dispatchers.Default, Channel.UNLIMITED) {
- try {
- for (command in channel) {
- when (command) {
- is Command.RegisterListener -> {
- intermittentDaemon.await()
-
- registerListener(command.listener)
- }
- is Command.UnregisterListener -> unregisterListener(command.listenerId)
- }
- }
- } catch (exception: ClosedReceiveChannelException) {
- // Registration queue closed; stop registrator
- }
- }
-
- private fun registerListener(listener: Messenger) {
- synchronized(this) {
- val listenerId = newListenerId()
-
- listeners.put(listenerId, listener)
-
- val initialEvents =
- mutableListOf(
- Event.TunnelStateChange(connectionProxy.state),
- Event.AccountHistoryEvent(accountCache.onAccountHistoryChange.latestEvent),
- Event.SettingsUpdate(settingsListener.settings),
- Event.NewLocation(locationInfoCache.location),
- Event.SplitTunnelingUpdate(splitTunneling.onChange.latestEvent),
- Event.CurrentVersion(appVersionInfoCache.currentVersion),
- Event.AppVersionInfo(appVersionInfoCache.appVersionInfo),
- Event.NewRelayList(relayListListener.relayList),
- Event.AuthToken(authTokenCache.authToken),
- Event.ListenerReady(messenger, listenerId)
- )
-
- if (vpnPermission.waitingForResponse) {
- initialEvents.add(Event.VpnPermissionRequest)
- }
-
- val didSuccessfullySendAllMessages =
- initialEvents.all { event ->
- listener.trySendEvent(event, SHOULD_LOG_DEAD_OBJECT_EXCEPTION)
- }
- if (didSuccessfullySendAllMessages.not()) {
- listeners.remove(listenerId)
- }
- }
- }
-
- private fun unregisterListener(listenerId: Int) {
- synchronized(this) { listeners.remove(listenerId) }
- }
-
- private fun newListenerId(): Int {
- val listenerId = listenerIdCounter
-
- listenerIdCounter += 1
-
- return listenerId
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/SettingsListener.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/SettingsListener.kt
deleted file mode 100644
index 2863594cb9..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/SettingsListener.kt
+++ /dev/null
@@ -1,139 +0,0 @@
-package net.mullvad.mullvadvpn.service.endpoint
-
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.channels.ClosedReceiveChannelException
-import kotlinx.coroutines.channels.actor
-import kotlinx.coroutines.channels.trySendBlocking
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.lib.ipc.Request
-import net.mullvad.mullvadvpn.model.*
-import net.mullvad.mullvadvpn.service.MullvadDaemon
-import net.mullvad.talpid.util.EventNotifier
-
-class SettingsListener(endpoint: ServiceEndpoint) {
- private sealed class Command {
- class SetAllowLan(val allow: Boolean) : Command()
- class SetAutoConnect(val autoConnect: Boolean) : Command()
- class SetWireGuardMtu(val mtu: Int?) : Command()
- class SetObfuscationSettings(val settings: ObfuscationSettings?) : Command()
- class SetQuantumResistant(val quantumResistant: QuantumResistantState) : Command()
- }
-
- private val commandChannel = spawnActor()
- private val daemon = endpoint.intermittentDaemon
-
- val dnsOptionsNotifier = EventNotifier<DnsOptions?>(null)
- val relaySettingsNotifier = EventNotifier<RelaySettings?>(null)
- val obfuscationSettingsNotifier = EventNotifier<ObfuscationSettings?>(null)
- val settingsNotifier = EventNotifier<Settings?>(null)
-
- var settings by settingsNotifier.notifiable()
- private set
-
- init {
- daemon.registerListener(this) { newDaemon ->
- if (newDaemon != null) {
- registerListener(newDaemon)
- fetchInitialSettings(newDaemon)
- }
- }
-
- settingsNotifier.subscribe(this) { settings ->
- endpoint.sendEvent(Event.SettingsUpdate(settings))
- }
-
- endpoint.dispatcher.apply {
- registerHandler(Request.SetAllowLan::class) { request ->
- commandChannel.trySendBlocking(Command.SetAllowLan(request.allow))
- }
-
- registerHandler(Request.SetAutoConnect::class) { request ->
- commandChannel.trySendBlocking(Command.SetAutoConnect(request.autoConnect))
- }
-
- registerHandler(Request.SetWireGuardMtu::class) { request ->
- commandChannel.trySendBlocking(Command.SetWireGuardMtu(request.mtu))
- }
-
- registerHandler(Request.SetObfuscationSettings::class) { request ->
- commandChannel.trySendBlocking(Command.SetObfuscationSettings(request.settings))
- }
-
- registerHandler(Request.SetWireGuardQuantumResistant::class) { request ->
- commandChannel.trySendBlocking(
- Command.SetQuantumResistant(request.quantumResistant)
- )
- }
- }
- }
-
- fun onDestroy() {
- commandChannel.close()
- daemon.unregisterListener(this)
-
- dnsOptionsNotifier.unsubscribeAll()
- relaySettingsNotifier.unsubscribeAll()
- obfuscationSettingsNotifier.unsubscribeAll()
- settingsNotifier.unsubscribeAll()
- }
-
- fun subscribe(id: Any, listener: (Settings) -> Unit) {
- settingsNotifier.subscribe(id) { maybeSettings ->
- maybeSettings?.let { settings -> listener(settings) }
- }
- }
-
- fun unsubscribe(id: Any) {
- settingsNotifier.unsubscribe(id)
- }
-
- private fun registerListener(daemon: MullvadDaemon) {
- daemon.onSettingsChange.subscribe(this, ::handleNewSettings)
- }
-
- private fun fetchInitialSettings(daemon: MullvadDaemon) {
- synchronized(this) { handleNewSettings(daemon.getSettings()) }
- }
-
- private fun handleNewSettings(newSettings: Settings?) {
- if (newSettings != null) {
- synchronized(this) {
- if (settings?.tunnelOptions?.dnsOptions != newSettings.tunnelOptions.dnsOptions) {
- dnsOptionsNotifier.notify(newSettings.tunnelOptions.dnsOptions)
- }
-
- if (settings?.relaySettings != newSettings.relaySettings) {
- relaySettingsNotifier.notify(newSettings.relaySettings)
- }
-
- if (settings?.obfuscationSettings != newSettings.obfuscationSettings) {
- obfuscationSettingsNotifier.notify(newSettings.obfuscationSettings)
- }
-
- settings = newSettings
- }
- }
- }
-
- private fun spawnActor() =
- GlobalScope.actor<Command>(Dispatchers.Default, Channel.UNLIMITED) {
- try {
- for (command in channel) {
- when (command) {
- is Command.SetAllowLan -> daemon.await().setAllowLan(command.allow)
- is Command.SetAutoConnect ->
- daemon.await().setAutoConnect(command.autoConnect)
- is Command.SetWireGuardMtu -> daemon.await().setWireguardMtu(command.mtu)
- is Command.SetObfuscationSettings ->
- daemon.await().setObfuscationSettings(command.settings)
- is Command.SetQuantumResistant ->
- daemon.await().setQuantumResistant(command.quantumResistant)
- }
- }
- } catch (exception: ClosedReceiveChannelException) {
- // Closed sender, so stop the actor
- }
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/SplitTunneling.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/SplitTunneling.kt
deleted file mode 100644
index a683b1e4bf..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/SplitTunneling.kt
+++ /dev/null
@@ -1,59 +0,0 @@
-package net.mullvad.mullvadvpn.service.endpoint
-
-import kotlin.properties.Delegates.observable
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.lib.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) { _, wasEnabled, isEnabled ->
- if (wasEnabled != isEnabled) {
- persistence.enabled = isEnabled
- update()
- }
- }
-
- val onChange = EventNotifier<List<String>?>(excludedApps.toList())
-
- 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/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/VoucherRedeemer.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/VoucherRedeemer.kt
deleted file mode 100644
index a7003d6888..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/VoucherRedeemer.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-package net.mullvad.mullvadvpn.service.endpoint
-
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.channels.ClosedReceiveChannelException
-import kotlinx.coroutines.channels.actor
-import kotlinx.coroutines.channels.trySendBlocking
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.lib.ipc.Request
-
-class VoucherRedeemer(private val endpoint: ServiceEndpoint) {
- private val daemon
- get() = endpoint.intermittentDaemon
-
- private val voucherChannel = spawnActor()
-
- init {
- endpoint.dispatcher.registerHandler(Request.SubmitVoucher::class) { request ->
- voucherChannel.trySendBlocking(request.voucher)
- }
- }
-
- fun onDestroy() {
- voucherChannel.close()
- }
-
- private fun spawnActor() =
- GlobalScope.actor<String>(Dispatchers.Default, Channel.UNLIMITED) {
- try {
- for (voucher in channel) {
- val result = daemon.await().submitVoucher(voucher)
-
- endpoint.sendEvent(Event.VoucherSubmissionResult(voucher, result))
- }
- } catch (exception: ClosedReceiveChannelException) {
- // Voucher channel was closed, stop the actor
- }
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/VpnPermission.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/VpnPermission.kt
deleted file mode 100644
index d94d1d6b60..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/VpnPermission.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-package net.mullvad.mullvadvpn.service.endpoint
-
-import android.content.Context
-import android.content.Intent
-import android.net.VpnService
-import net.mullvad.mullvadvpn.lib.common.util.Intermittent
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.lib.ipc.Request
-import net.mullvad.mullvadvpn.ui.MainActivity
-
-class VpnPermission(private val context: Context, private val endpoint: ServiceEndpoint) {
- private val isGranted = Intermittent<Boolean>()
-
- var waitingForResponse = false
- private set
-
- init {
- endpoint.dispatcher.registerHandler(Request.VpnPermissionResponse::class) { request ->
- waitingForResponse = false
- isGranted.spawnUpdate(request.isGranted)
- }
- }
-
- suspend fun request(): Boolean {
- val intent = VpnService.prepare(context)
-
- if (intent == null) {
- isGranted.update(true)
- } else {
- val activityIntent =
- Intent(context, MainActivity::class.java).apply {
- addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
- }
-
- isGranted.update(null)
- waitingForResponse = true
-
- context.startActivity(activityIntent)
- endpoint.sendEvent(Event.VpnPermissionRequest)
- }
-
- return isGranted.await()
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/AccountExpiryNotification.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/AccountExpiryNotification.kt
deleted file mode 100644
index b66c668972..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/AccountExpiryNotification.kt
+++ /dev/null
@@ -1,142 +0,0 @@
-package net.mullvad.mullvadvpn.service.notifications
-
-import android.app.Notification
-import android.app.NotificationManager
-import android.app.PendingIntent
-import android.content.Context
-import android.content.Intent
-import android.net.Uri
-import androidx.core.app.NotificationCompat
-import kotlin.properties.Delegates.observable
-import kotlinx.coroutines.delay
-import net.mullvad.mullvadvpn.BuildConfig
-import net.mullvad.mullvadvpn.R
-import net.mullvad.mullvadvpn.lib.common.constant.BuildTypes
-import net.mullvad.mullvadvpn.lib.common.util.Intermittent
-import net.mullvad.mullvadvpn.lib.common.util.JobTracker
-import net.mullvad.mullvadvpn.lib.common.util.SdkUtils
-import net.mullvad.mullvadvpn.lib.common.util.SdkUtils.isNotificationPermissionGranted
-import net.mullvad.mullvadvpn.model.AccountExpiry
-import net.mullvad.mullvadvpn.service.MullvadDaemon
-import net.mullvad.mullvadvpn.service.endpoint.AccountCache
-import net.mullvad.mullvadvpn.ui.MainActivity
-import org.joda.time.DateTime
-import org.joda.time.Duration
-
-class AccountExpiryNotification(
- val context: Context,
- val daemon: Intermittent<MullvadDaemon>,
- val accountCache: AccountCache
-) {
- companion object {
- val NOTIFICATION_ID: Int = 2
- val REMAINING_TIME_FOR_REMINDERS = Duration.standardDays(2)
- val TIME_BETWEEN_CHECKS: Long = 12 /* h */ * 60 /* min */ * 60 /* s */ * 1000 /* ms */
- }
-
- private val jobTracker = JobTracker()
- private val resources = context.resources
-
- private val buyMoreTimeUrl = resources.getString(R.string.account_url)
-
- private val channel =
- NotificationChannel(
- context,
- "mullvad_account_time",
- NotificationCompat.VISIBILITY_PRIVATE,
- R.string.account_time_notification_channel_name,
- R.string.account_time_notification_channel_description,
- NotificationManager.IMPORTANCE_HIGH,
- true,
- true
- )
-
- var accountExpiry by
- observable<AccountExpiry>(AccountExpiry.Missing) { _, oldValue, newValue ->
- if (oldValue != newValue) {
- jobTracker.newUiJob("update") { update(newValue) }
- }
- }
-
- init {
- accountCache.onAccountExpiryChange.subscribe(this) { expiry -> accountExpiry = expiry }
- }
-
- fun onDestroy() {
- accountCache.onAccountExpiryChange.unsubscribe(this)
- }
-
- private suspend fun update(expiry: AccountExpiry) {
- val expiryDate = expiry.date()
- val durationUntilExpiry = expiryDate?.remainingTime()
-
- if (accountCache.isNewAccount.not() && durationUntilExpiry?.isCloseToExpiry() == true) {
- if (context.isNotificationPermissionGranted()) {
- val notification = build(expiryDate, durationUntilExpiry)
- channel.notificationManager.notify(NOTIFICATION_ID, notification)
- }
- jobTracker.newUiJob("scheduleUpdate") { scheduleUpdate() }
- } else {
- channel.notificationManager.cancel(NOTIFICATION_ID)
- jobTracker.cancelJob("scheduleUpdate")
- }
- }
-
- private fun DateTime.remainingTime(): Duration {
- return Duration(DateTime.now(), this)
- }
-
- private fun Duration.isCloseToExpiry(): Boolean {
- return isShorterThan(REMAINING_TIME_FOR_REMINDERS)
- }
-
- private suspend fun scheduleUpdate() {
- delay(TIME_BETWEEN_CHECKS)
- update(accountExpiry)
- }
-
- private suspend fun build(expiry: DateTime, remainingTime: Duration): Notification {
- val url =
- jobTracker.runOnBackground {
- Uri.parse("$buyMoreTimeUrl?token=${daemon.await().getWwwAuthToken()}")
- }
- val intent =
- if (BuildTypes.RELEASE == BuildConfig.BUILD_TYPE) {
- Intent(context, MainActivity::class.java)
- .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
- .setAction(Intent.ACTION_MAIN)
- } else {
- Intent(Intent.ACTION_VIEW, url)
- }
- val pendingIntent =
- PendingIntent.getActivity(context, 1, intent, SdkUtils.getSupportedPendingIntentFlags())
-
- return channel.buildNotification(pendingIntent, format(expiry, remainingTime))
- }
-
- private fun format(expiry: DateTime, remainingTime: Duration): String {
- if (remainingTime.isShorterThan(Duration.ZERO)) {
- return resources.getString(R.string.account_credit_has_expired)
- } else {
- val remainingTimeInfo = remainingTime.toPeriodTo(expiry)
-
- if (remainingTimeInfo.days >= 1) {
- return getRemainingText(
- R.plurals.account_credit_expires_in_days,
- remainingTime.standardDays.toInt()
- )
- } else if (remainingTimeInfo.hours >= 1) {
- return getRemainingText(
- R.plurals.account_credit_expires_in_hours,
- remainingTime.standardHours.toInt()
- )
- } else {
- return resources.getString(R.string.account_credit_expires_in_a_few_minutes)
- }
- }
- }
-
- private fun getRemainingText(pluralId: Int, quantity: Int): String {
- return resources.getQuantityString(pluralId, quantity, quantity)
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/NotificationChannel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/NotificationChannel.kt
deleted file mode 100644
index de557aaf22..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/NotificationChannel.kt
+++ /dev/null
@@ -1,97 +0,0 @@
-package net.mullvad.mullvadvpn.service.notifications
-
-import android.app.Notification
-import android.app.PendingIntent
-import android.content.Context
-import androidx.core.app.NotificationChannelCompat
-import androidx.core.app.NotificationCompat
-import androidx.core.app.NotificationManagerCompat
-import net.mullvad.mullvadvpn.R
-
-class NotificationChannel(
- val context: Context,
- val id: String,
- val visibility: Int,
- name: Int,
- description: Int,
- importance: Int,
- isVibrationEnabled: Boolean,
- isBadgeEnabled: Boolean
-) {
- private val badgeColor by lazy { context.getColor(R.color.colorPrimary) }
-
- val notificationManager = NotificationManagerCompat.from(context)
-
- init {
- val channelName = context.getString(name)
- val channelDescription = context.getString(description)
-
- val channel =
- NotificationChannelCompat.Builder(id, importance)
- .setName(channelName)
- .setDescription(channelDescription)
- .setShowBadge(isBadgeEnabled)
- .setVibrationEnabled(isVibrationEnabled)
- .build()
-
- notificationManager.createNotificationChannel(channel)
- }
-
- fun buildNotification(
- intent: PendingIntent,
- title: String,
- deleteIntent: PendingIntent? = null,
- isOngoing: Boolean = false
- ): Notification {
- return buildNotification(intent, title, emptyList(), deleteIntent, isOngoing)
- }
-
- fun buildNotification(
- intent: PendingIntent,
- title: Int,
- deleteIntent: PendingIntent? = null,
- isOngoing: Boolean = false
- ): Notification {
- return buildNotification(intent, title, emptyList(), deleteIntent, isOngoing)
- }
-
- fun buildNotification(
- pendingIntent: PendingIntent,
- title: Int,
- actions: List<NotificationCompat.Action>,
- deleteIntent: PendingIntent? = null,
- isOngoing: Boolean = false
- ): Notification {
- return buildNotification(
- pendingIntent,
- context.getString(title),
- actions,
- deleteIntent,
- isOngoing
- )
- }
-
- private fun buildNotification(
- pendingIntent: PendingIntent,
- title: String,
- actions: List<NotificationCompat.Action>,
- deleteIntent: PendingIntent? = null,
- isOngoing: Boolean = false
- ): Notification {
- val builder =
- NotificationCompat.Builder(context, id)
- .setSmallIcon(R.drawable.small_logo_black)
- .setColor(badgeColor)
- .setContentTitle(title)
- .setContentIntent(pendingIntent)
- .setVisibility(visibility)
- .setOngoing(isOngoing)
- for (action in actions) {
- builder.addAction(action)
- }
-
- deleteIntent?.let { intent -> builder.setDeleteIntent(intent) }
-
- return builder.build()
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/TunnelStateNotification.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/TunnelStateNotification.kt
deleted file mode 100644
index b25d1a2056..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/TunnelStateNotification.kt
+++ /dev/null
@@ -1,142 +0,0 @@
-package net.mullvad.mullvadvpn.service.notifications
-
-import android.app.Notification
-import android.app.NotificationManager
-import android.app.PendingIntent
-import android.content.Context
-import android.content.Intent
-import androidx.core.app.NotificationCompat
-import kotlin.properties.Delegates.observable
-import net.mullvad.mullvadvpn.R
-import net.mullvad.mullvadvpn.lib.common.util.SdkUtils
-import net.mullvad.mullvadvpn.lib.common.util.getErrorNotificationResources
-import net.mullvad.mullvadvpn.model.TunnelState
-import net.mullvad.mullvadvpn.ui.MainActivity
-import net.mullvad.talpid.tunnel.ActionAfterDisconnect
-import net.mullvad.talpid.tunnel.ErrorStateCause
-
-class TunnelStateNotification(val context: Context) {
- companion object {
- val NOTIFICATION_ID: Int = 1
- }
-
- private val channel =
- NotificationChannel(
- context,
- "vpn_tunnel_status",
- NotificationCompat.VISIBILITY_SECRET,
- R.string.foreground_notification_channel_name,
- R.string.foreground_notification_channel_description,
- NotificationManager.IMPORTANCE_MIN,
- false,
- false
- )
-
- private val notificationText: Int
- get() =
- when (val state = tunnelState) {
- is TunnelState.Disconnected -> R.string.unsecured
- is TunnelState.Connecting -> {
- if (reconnecting) {
- R.string.reconnecting
- } else {
- R.string.connecting
- }
- }
- is TunnelState.Connected -> R.string.secured
- is TunnelState.Disconnecting -> {
- when (state.actionAfterDisconnect) {
- ActionAfterDisconnect.Reconnect -> R.string.reconnecting
- else -> R.string.disconnecting
- }
- }
- is TunnelState.Error -> {
- if (state.isDeviceOffline()) {
- R.string.blocking_internet_device_offline
- } else {
- state.errorState.getErrorNotificationResources(context).titleResourceId
- }
- }
- }
-
- private fun TunnelState.isDeviceOffline(): Boolean {
- return (this as? TunnelState.Error)?.errorState?.cause is ErrorStateCause.IsOffline
- }
-
- private val shouldDisplayOngoingNotification: Boolean
- get() =
- when (tunnelState) {
- is TunnelState.Connected -> true
- is TunnelState.Disconnected,
- is TunnelState.Connecting,
- is TunnelState.Disconnecting,
- is TunnelState.Error -> false
- }
-
- private var reconnecting = false
- private var showingReconnecting = false
-
- var showAction by observable(false) { _, _, _ -> update() }
-
- var tunnelState by
- observable<TunnelState>(TunnelState.Disconnected) { _, _, newState ->
- val isReconnecting = newState is TunnelState.Connecting && reconnecting
- val shouldBeginReconnecting =
- (newState as? TunnelState.Disconnecting)?.actionAfterDisconnect ==
- ActionAfterDisconnect.Reconnect
- reconnecting = isReconnecting || shouldBeginReconnecting
- update()
- }
-
- var visible by
- observable(true) { _, _, newValue ->
- if (newValue == true) {
- update()
- } else {
- channel.notificationManager.cancel(NOTIFICATION_ID)
- }
- }
-
- private fun update() {
- if (visible && (!reconnecting || !showingReconnecting)) {
- channel.notificationManager.notify(NOTIFICATION_ID, build())
- }
- }
-
- fun build(): Notification {
- val intent =
- Intent(context, MainActivity::class.java)
- .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
- .setAction(Intent.ACTION_MAIN)
- val pendingIntent =
- PendingIntent.getActivity(context, 1, intent, SdkUtils.getSupportedPendingIntentFlags())
- val actions =
- if (showAction) {
- listOf(buildAction())
- } else {
- emptyList()
- }
-
- return channel.buildNotification(
- pendingIntent,
- notificationText,
- actions,
- isOngoing = shouldDisplayOngoingNotification
- )
- }
-
- private fun buildAction(): NotificationCompat.Action {
- val action = TunnelStateNotificationAction.from(tunnelState)
- val label = context.getString(action.text)
- val intent = Intent(action.key).setPackage("net.mullvad.mullvadvpn")
- val pendingIntent =
- PendingIntent.getForegroundService(
- context,
- 1,
- intent,
- SdkUtils.getSupportedPendingIntentFlags()
- )
-
- return NotificationCompat.Action(action.icon, label, pendingIntent)
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/TunnelStateNotificationAction.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/TunnelStateNotificationAction.kt
deleted file mode 100644
index 9ed9998054..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/TunnelStateNotificationAction.kt
+++ /dev/null
@@ -1,58 +0,0 @@
-package net.mullvad.mullvadvpn.service.notifications
-
-import net.mullvad.mullvadvpn.R
-import net.mullvad.mullvadvpn.model.TunnelState
-import net.mullvad.mullvadvpn.service.MullvadVpnService
-import net.mullvad.talpid.tunnel.ActionAfterDisconnect
-
-enum class TunnelStateNotificationAction {
- Connect,
- Disconnect,
- Cancel,
- Dismiss;
-
- companion object {
- fun from(tunnelState: TunnelState) =
- when (tunnelState) {
- is TunnelState.Disconnected -> Connect
- is TunnelState.Connecting -> Cancel
- is TunnelState.Connected -> Disconnect
- is TunnelState.Disconnecting -> {
- when (tunnelState.actionAfterDisconnect) {
- ActionAfterDisconnect.Reconnect -> Cancel
- else -> Connect
- }
- }
- is TunnelState.Error -> {
- if (tunnelState.errorState.isBlocking) {
- Disconnect
- } else {
- Dismiss
- }
- }
- }
- }
-
- val text
- get() =
- when (this) {
- Connect -> R.string.connect
- Disconnect -> R.string.disconnect
- Cancel -> R.string.cancel
- Dismiss -> R.string.dismiss
- }
-
- val key
- get() =
- when (this) {
- Connect -> MullvadVpnService.KEY_CONNECT_ACTION
- else -> MullvadVpnService.KEY_DISCONNECT_ACTION
- }
-
- val icon
- get() =
- when (this) {
- Connect -> R.drawable.icon_notification_connect
- else -> R.drawable.icon_notification_disconnect
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/persistence/SplitTunnelingPersistence.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/persistence/SplitTunnelingPersistence.kt
deleted file mode 100644
index 264304ab3f..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/persistence/SplitTunnelingPersistence.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-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()
- }
- }
-}