diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/autotunnel/AutoTunnelService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/autotunnel/AutoTunnelService.kt index b4028c6b..07156c4d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/autotunnel/AutoTunnelService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/autotunnel/AutoTunnelService.kt @@ -9,7 +9,6 @@ import androidx.lifecycle.LifecycleService import androidx.lifecycle.lifecycleScope import com.wireguard.android.util.RootShell import com.zaneschepke.wireguardautotunnel.R -import com.zaneschepke.wireguardautotunnel.data.domain.Settings import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.module.AppShell @@ -23,7 +22,6 @@ import com.zaneschepke.wireguardautotunnel.service.network.NetworkStatus import com.zaneschepke.wireguardautotunnel.service.network.WifiService import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService -import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.extensions.cancelWithMessage import com.zaneschepke.wireguardautotunnel.util.extensions.getCurrentWifiName @@ -35,6 +33,7 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.update @@ -42,6 +41,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber import java.net.InetAddress +import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject import javax.inject.Provider @@ -86,11 +86,9 @@ class AutoTunnelService : LifecycleService() { private var wakeLock: PowerManager.WakeLock? = null - private var wifiJob: Job? = null - private var mobileDataJob: Job? = null - private var ethernetJob: Job? = null + private val pingTunnelRestartActive = AtomicBoolean(false) + private var pingJob: Job? = null - private var networkEventJob: Job? = null override fun onCreate() { super.onCreate() @@ -123,6 +121,8 @@ class AutoTunnelService : LifecycleService() { } startSettingsJob() startVpnStateJob() + startNetworkJobs() + startPingStateJob() }.onFailure { Timber.e(it) } @@ -138,7 +138,6 @@ class AutoTunnelService : LifecycleService() { } override fun onDestroy() { - cancelAndResetNetworkJobs() cancelAndResetPingJob() serviceManager.autoTunnelService = CompletableDeferred() super.onDestroy() @@ -203,6 +202,16 @@ class AutoTunnelService : LifecycleService() { handleNetworkEventChanges() } + private fun startPingStateJob() = lifecycleScope.launch { + autoTunnelStateFlow.collect { + if (it.isPingEnabled()) { + pingJob.onNotRunning { pingJob = startPingJob() } + } else { + if (!pingTunnelRestartActive.get()) cancelAndResetPingJob() + } + } + } + private suspend fun watchForMobileDataConnectivityChanges() { withContext(ioDispatcher) { Timber.i("Starting mobile data watcher") @@ -232,8 +241,8 @@ class AutoTunnelService : LifecycleService() { Timber.i("Starting ping watcher") runCatching { do { - val vpnState = tunnelService.get().vpnState.value - if (vpnState.status == TunnelState.UP) { + val vpnState = autoTunnelStateFlow.value.vpnState + if (vpnState.status.isUp() && !autoTunnelStateFlow.value.isNoConnectivity()) { if (vpnState.tunnelConfig != null) { val config = TunnelConfig.configFromWgQuick(vpnState.tunnelConfig.wgQuick) val results = if (vpnState.tunnelConfig.pingIp != null) { @@ -249,7 +258,9 @@ class AutoTunnelService : LifecycleService() { if (results.contains(false)) { Timber.i("Restarting VPN for ping failure") val cooldown = vpnState.tunnelConfig.pingCooldown + pingTunnelRestartActive.set(true) tunnelService.get().bounceTunnel() + pingTunnelRestartActive.set(false) delay(cooldown ?: Constants.PING_COOLDOWN) continue } @@ -267,14 +278,10 @@ class AutoTunnelService : LifecycleService() { Timber.i("Starting settings watcher") withContext(ioDispatcher) { appDataRepository.settings.getSettingsFlow().combine( - // ignore isActive changes to allow manual tunnel overrides - appDataRepository.tunnels.getTunnelConfigsFlow().distinctUntilChanged { old, new -> - old.map { it.isActive } != new.map { it.isActive } - }, + appDataRepository.tunnels.getTunnelConfigsFlow(), ) { settings, tunnels -> Pair(settings, tunnels) }.collect { pair -> - manageJobsBySettings(pair.first) autoTunnelStateFlow.update { it.copy( settings = pair.first, @@ -292,53 +299,16 @@ class AutoTunnelService : LifecycleService() { autoTunnelStateFlow.update { it.copy(vpnState = state) } - // TODO think about this - // What happens if we change the pinger setting while vpn is active? - state.tunnelConfig?.let { - val settings = appDataRepository.settings.getSettings() - if (it.isPingEnabled && !settings.isPingEnabled) { - pingJob.onNotRunning { pingJob = startPingJob() } - } - if (!it.isPingEnabled && !settings.isPingEnabled) { - cancelAndResetPingJob() - } - } - } - } - } - - private fun manageJobsBySettings(settings: Settings) { - with(settings) { - if (isPingEnabled) { - pingJob.onNotRunning { pingJob = startPingJob() } - } else { - cancelAndResetPingJob() - } - if (isTunnelOnWifiEnabled || isTunnelOnEthernetEnabled || isTunnelOnMobileDataEnabled) { - startNetworkJobs() - } else { - cancelAndResetNetworkJobs() } } } private fun startNetworkJobs() { - wifiJob.onNotRunning { - Timber.i("Wifi job starting") - wifiJob = startWifiJob() - } - ethernetJob.onNotRunning { - ethernetJob = startEthernetJob() - Timber.i("Ethernet job starting") - } - mobileDataJob.onNotRunning { - mobileDataJob = startMobileDataJob() - Timber.i("Mobile data job starting") - } - networkEventJob.onNotRunning { - Timber.i("Network event job starting") - networkEventJob = startNetworkEventJob() - } + Timber.i("Starting all network state jobs..") + startWifiJob() + startEthernetJob() + startMobileDataJob() + startNetworkEventJob() } private fun cancelAndResetPingJob() { @@ -346,17 +316,6 @@ class AutoTunnelService : LifecycleService() { pingJob = null } - private fun cancelAndResetNetworkJobs() { - networkEventJob?.cancelWithMessage("Network event job canceled") - wifiJob?.cancelWithMessage("Wifi job canceled") - ethernetJob?.cancelWithMessage("Ethernet job canceled") - mobileDataJob?.cancelWithMessage("Mobile data job canceled") - networkEventJob = null - wifiJob = null - ethernetJob = null - mobileDataJob = null - } - private fun emitEthernetConnected(connected: Boolean) { autoTunnelStateFlow.update { it.copy( @@ -459,19 +418,16 @@ class AutoTunnelService : LifecycleService() { private suspend fun handleNetworkEventChanges() { withContext(ioDispatcher) { Timber.i("Starting auto-tunnel network event watcher") - // allow manual overrides + // ignore vpnState emits to allow manual overrides autoTunnelStateFlow.distinctUntilChanged { old, new -> - old.copy(vpnState = new.vpnState) == new + old.copy(vpnState = new.vpnState) == new || old.tunnels.map { it.isActive } != new.tunnels.map { it.isActive } }.collect { watcherState -> when (val event = watcherState.asAutoTunnelEvent()) { - is AutoTunnelEvent.Start -> { - Timber.d("Start tunnel ${event.tunnelConfig?.name}") - tunnelService.get().startTunnel(event.tunnelConfig ?: appDataRepository.getPrimaryOrFirstTunnel()) - } - is AutoTunnelEvent.Stop -> { - Timber.d("Stop tunnel") - tunnelService.get().stopTunnel() - } + is AutoTunnelEvent.Start -> tunnelService.get().startTunnel( + event.tunnelConfig + ?: appDataRepository.getPrimaryOrFirstTunnel(), + ) + is AutoTunnelEvent.Stop -> tunnelService.get().stopTunnel() AutoTunnelEvent.DoNothing -> Timber.i("Auto-tunneling: no condition met") } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/autotunnel/AutoTunnelState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/autotunnel/AutoTunnelState.kt index de9b1039..f9fa3c39 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/autotunnel/AutoTunnelState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/autotunnel/AutoTunnelState.kt @@ -51,11 +51,11 @@ data class AutoTunnelState( return isEthernetConnected && settings.isTunnelOnEthernetEnabled && vpnState.status.isDown() } - private fun stopOnEthernet() : Boolean { + private fun stopOnEthernet(): Boolean { return isEthernetConnected && !settings.isTunnelOnEthernetEnabled && vpnState.status.isUp() } - private fun isNoConnectivity(): Boolean { + fun isNoConnectivity(): Boolean { return !isEthernetConnected && !isWifiConnected && !isMobileDataConnected } @@ -96,7 +96,9 @@ data class AutoTunnelState( val vpnTunnel = vpnState.tunnelConfig return if (preferred != null && vpnTunnel != null) { preferred.id == vpnTunnel.id - } else true + } else { + true + } } fun asAutoTunnelEvent(): AutoTunnelEvent { @@ -120,14 +122,23 @@ data class AutoTunnelState( private fun isCurrentSSIDTrusted(): Boolean { return if (settings.isWildcardsEnabled) { settings.trustedNetworkSSIDs.isMatchingToWildcardList(currentNetworkSSID) - } else settings.trustedNetworkSSIDs.contains(currentNetworkSSID) + } else { + settings.trustedNetworkSSIDs.contains(currentNetworkSSID) + } } private fun getTunnelWithMatchingTunnelNetwork(): TunnelConfig? { return tunnels.firstOrNull { if (settings.isWildcardsEnabled) { it.tunnelNetworks.isMatchingToWildcardList(currentNetworkSSID) - } else it.tunnelNetworks.contains(currentNetworkSSID) + } else { + it.tunnelNetworks.contains(currentNetworkSSID) + } } } + + fun isPingEnabled(): Boolean { + return settings.isPingEnabled || + (vpnState.status.isUp() && vpnState.tunnelConfig != null && tunnels.first { it.id == vpnState.tunnelConfig.id }.isPingEnabled) + } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt index f0003252..d5d0bc34 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt @@ -252,7 +252,7 @@ constructor( fun onCopyTunnel(tunnel: TunnelConfig) = viewModelScope.launch { saveTunnel( - TunnelConfig(name = makeTunnelNameUnique(tunnel.name), wgQuick = tunnel.wgQuick, amQuick = tunnel.amQuick) + TunnelConfig(name = makeTunnelNameUnique(tunnel.name), wgQuick = tunnel.wgQuick, amQuick = tunnel.amQuick), ) } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/autotunnel/AutoTunnelScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/autotunnel/AutoTunnelScreen.kt index 6a741277..b84e82c5 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/autotunnel/AutoTunnelScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/autotunnel/AutoTunnelScreen.kt @@ -72,16 +72,18 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV isBackgroundLocationGranted = fineLocationState.status.isGranted } - fun onAutoTunnelWifiChecked() { - if (uiState.settings.isTunnelOnWifiEnabled) viewModel.onToggleTunnelOnWifi().also { return } - when (false) { - isBackgroundLocationGranted -> showLocationDialog = true - fineLocationState.status.isGranted -> showLocationDialog = true - context.isLocationServicesEnabled() -> - showLocationServicesAlertDialog = true - else -> { - viewModel.onToggleTunnelOnWifi() + fun isWifiNameReadable(): Boolean { + return when { + !isBackgroundLocationGranted || + !fineLocationState.status.isGranted -> { + showLocationDialog = true + false } + !context.isLocationServicesEnabled() -> { + showLocationServicesAlertDialog = true + false + } + else -> true } } @@ -118,14 +120,14 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV topBar = { TopNavBar(stringResource(R.string.auto_tunneling)) }, - ) { + ) { padding -> Column( horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.spacedBy(24.dp.scaledHeight(), Alignment.Top), modifier = Modifier .fillMaxSize() - .padding(it) + .padding(padding) .padding(top = 24.dp.scaledHeight()) .padding(horizontal = 24.dp.scaledWidth()), ) { @@ -148,14 +150,12 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV enabled = !uiState.settings.isAlwaysOnVpnEnabled, checked = uiState.settings.isTunnelOnWifiEnabled, onClick = { - if (uiState.settings.isWifiNameByShellEnabled) viewModel.onToggleTunnelOnWifi().also { return@ScaledSwitch } - onAutoTunnelWifiChecked() + viewModel.onToggleTunnelOnWifi() }, ) }, onClick = { - if (uiState.settings.isWifiNameByShellEnabled) viewModel.onToggleTunnelOnWifi().also { return@SelectionItem } - onAutoTunnelWifiChecked() + viewModel.onToggleTunnelOnWifi() }, ), SelectionItem( @@ -251,7 +251,9 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV uiState.settings.trustedNetworkSSIDs, onDelete = viewModel::onDeleteTrustedSSID, currentText = currentText, - onSave = viewModel::onSaveTrustedSSID, + onSave = { ssid -> + if (uiState.settings.isWifiNameByShellEnabled || isWifiNameReadable()) viewModel.onSaveTrustedSSID(ssid) + }, onValueChange = { currentText = it }, supporting = { if (uiState.settings.isWildcardsEnabled) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 299aa92b..a6a33e0e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -133,7 +133,7 @@ Feature requires at least one tunnel Allow all the time location permission and/or precise location is required for this feature. Please see app settings - to make sure these permissions are enabled. + to make sure these permissions are enabled Root shell accepted Set custom ping ip (optional, defaults to peers)