diff options
Diffstat (limited to 'android/app')
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() - } - } -} |
