fix: make ping aware of network availability

add basic tunnel error messages
This commit is contained in:
Zane Schepke 2025-01-02 16:47:23 -05:00
parent 53c09267df
commit b1dc6c5d59
19 changed files with 165 additions and 83 deletions

View File

@ -164,7 +164,7 @@
tools:node="merge" /> tools:node="merge" />
<service <service
android:name=".service.foreground.TunnelBackgroundService" android:name=".service.foreground.TunnelForegroundService"
android:exported="false" android:exported="false"
android:persistent="true" android:persistent="true"
android:foregroundServiceType="systemExempted" android:foregroundServiceType="systemExempted"

View File

@ -3,6 +3,9 @@ package com.zaneschepke.wireguardautotunnel
import android.app.Application import android.app.Application
import android.os.StrictMode import android.os.StrictMode
import android.os.StrictMode.ThreadPolicy import android.os.StrictMode.ThreadPolicy
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import com.zaneschepke.logcatter.LogReader import com.zaneschepke.logcatter.LogReader
import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository import com.zaneschepke.wireguardautotunnel.data.repository.SettingsRepository
@ -52,6 +55,7 @@ class WireGuardAutoTunnel : Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
instance = this instance = this
ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifecycleObserver())
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree()) Timber.plant(Timber.DebugTree())
StrictMode.setThreadPolicy( StrictMode.setThreadPolicy(
@ -88,7 +92,25 @@ class WireGuardAutoTunnel : Application() {
super.onTerminate() super.onTerminate()
} }
class AppLifecycleObserver : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) {
Timber.d("Application entered foreground")
foreground = true
}
override fun onPause(owner: LifecycleOwner) {
Timber.d("Application entered background")
foreground = false
}
}
companion object { companion object {
private var foreground = false
fun isForeground(): Boolean {
return foreground
}
lateinit var instance: WireGuardAutoTunnel lateinit var instance: WireGuardAutoTunnel
private set private set
} }

View File

@ -7,25 +7,21 @@ import com.zaneschepke.wireguardautotunnel.service.network.WifiService
import dagger.Binds import dagger.Binds
import dagger.Module import dagger.Module
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.components.ServiceComponent import dagger.hilt.components.SingletonComponent
import dagger.hilt.android.scopes.ServiceScoped
@Module @Module
@InstallIn(ServiceComponent::class) @InstallIn(SingletonComponent::class)
abstract class ServiceModule { abstract class ServiceModule {
@Binds @Binds
@Wifi @Wifi
@ServiceScoped
abstract fun provideWifiService(wifiService: WifiService): NetworkService abstract fun provideWifiService(wifiService: WifiService): NetworkService
@Binds @Binds
@MobileData @MobileData
@ServiceScoped
abstract fun provideMobileDataService(mobileDataService: MobileDataService): NetworkService abstract fun provideMobileDataService(mobileDataService: MobileDataService): NetworkService
@Binds @Binds
@Ethernet @Ethernet
@ServiceScoped
abstract fun provideEthernetService(ethernetService: EthernetService): NetworkService abstract fun provideEthernetService(ethernetService: EthernetService): NetworkService
} }

View File

@ -10,6 +10,7 @@ import com.wireguard.android.util.ToolsInstaller
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.WireGuardTunnel import com.zaneschepke.wireguardautotunnel.service.tunnel.WireGuardTunnel
@ -78,6 +79,9 @@ class TunnelModule {
@IoDispatcher ioDispatcher: CoroutineDispatcher, @IoDispatcher ioDispatcher: CoroutineDispatcher,
serviceManager: ServiceManager, serviceManager: ServiceManager,
notificationService: NotificationService, notificationService: NotificationService,
@Wifi wifiService: NetworkService,
@MobileData mobileDataService: NetworkService,
@Ethernet ethernetService: NetworkService,
): TunnelService { ): TunnelService {
return WireGuardTunnel( return WireGuardTunnel(
amneziaBackend, amneziaBackend,
@ -88,6 +92,9 @@ class TunnelModule {
ioDispatcher, ioDispatcher,
serviceManager, serviceManager,
notificationService, notificationService,
wifiService,
mobileDataService,
ethernetService,
) )
} }

View File

@ -42,7 +42,7 @@ class AppUpdateReceiver : BroadcastReceiver() {
} }
if (!settings.isAutoTunnelEnabled) { if (!settings.isAutoTunnelEnabled) {
val tunnels = appDataRepository.tunnels.getAll().filter { it.isActive } val tunnels = appDataRepository.tunnels.getAll().filter { it.isActive }
if (tunnels.isNotEmpty()) tunnelService.get().startTunnel(tunnels.first(), true) if (tunnels.isNotEmpty()) tunnelService.get().startTunnel(tunnels.first())
} }
} }
} }

View File

@ -41,7 +41,7 @@ class BootReceiver : BroadcastReceiver() {
val tunState = tunnelService.get().vpnState.value.status val tunState = tunnelService.get().vpnState.value.status
if (activeTunnels.isNotEmpty() && tunState != TunnelState.UP) { if (activeTunnels.isNotEmpty() && tunState != TunnelState.UP) {
Timber.i("Starting previously active tunnel") Timber.i("Starting previously active tunnel")
tunnelService.get().startTunnel(activeTunnels.first(), true) tunnelService.get().startTunnel(activeTunnels.first())
} }
if (isAutoTunnelEnabled) { if (isAutoTunnelEnabled) {
Timber.i("Starting watcher service from boot") Timber.i("Starting watcher service from boot")

View File

@ -29,7 +29,7 @@ class ServiceManager
val autoTunnelActive = _autoTunnelActive.asStateFlow() val autoTunnelActive = _autoTunnelActive.asStateFlow()
var autoTunnelService = CompletableDeferred<AutoTunnelService>() var autoTunnelService = CompletableDeferred<AutoTunnelService>()
var backgroundService = CompletableDeferred<TunnelBackgroundService>() var backgroundService = CompletableDeferred<TunnelForegroundService>()
var autoTunnelTile = CompletableDeferred<AutoTunnelControlTile>() var autoTunnelTile = CompletableDeferred<AutoTunnelControlTile>()
var tunnelControlTile = CompletableDeferred<TunnelControlTile>() var tunnelControlTile = CompletableDeferred<TunnelControlTile>()
@ -59,10 +59,10 @@ class ServiceManager
} }
} }
suspend fun startBackgroundService(tunnelConfig: TunnelConfig?) { suspend fun startBackgroundService(tunnelConfig: TunnelConfig) {
if (backgroundService.isCompleted) return if (backgroundService.isCompleted) return
kotlin.runCatching { kotlin.runCatching {
startService(TunnelBackgroundService::class.java, true) startService(TunnelForegroundService::class.java, true)
backgroundService.await() backgroundService.await()
backgroundService.getCompleted().start(tunnelConfig) backgroundService.getCompleted().start(tunnelConfig)
}.onFailure { }.onFailure {

View File

@ -16,7 +16,7 @@ import kotlinx.coroutines.CompletableDeferred
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class TunnelBackgroundService : LifecycleService() { class TunnelForegroundService : LifecycleService() {
@Inject @Inject
lateinit var notificationService: NotificationService lateinit var notificationService: NotificationService
@ -39,9 +39,9 @@ class TunnelBackgroundService : LifecycleService() {
return super.onStartCommand(intent, flags, startId) return super.onStartCommand(intent, flags, startId)
} }
fun start(tunnelConfig: TunnelConfig?) { fun start(tunnelConfig: TunnelConfig) {
ServiceCompat.startForeground( ServiceCompat.startForeground(
this, this@TunnelForegroundService,
NotificationService.KERNEL_SERVICE_NOTIFICATION_ID, NotificationService.KERNEL_SERVICE_NOTIFICATION_ID,
createNotification(tunnelConfig), createNotification(tunnelConfig),
Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID, Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID,

View File

@ -5,4 +5,8 @@ data class NetworkState(
val isMobileDataConnected: Boolean = false, val isMobileDataConnected: Boolean = false,
val isEthernetConnected: Boolean = false, val isEthernetConnected: Boolean = false,
val wifiName: String? = null, val wifiName: String? = null,
) ) {
fun hasNoCapabilities(): Boolean {
return !isWifiConnected && !isMobileDataConnected && !isEthernetConnected
}
}

View File

@ -45,7 +45,7 @@ class ShortcutsActivity : ComponentActivity() {
Timber.d("Shortcut action on name: ${tunnelConfig?.name}") Timber.d("Shortcut action on name: ${tunnelConfig?.name}")
tunnelConfig?.let { tunnelConfig?.let {
when (intent.action) { when (intent.action) {
Action.START.name -> tunnelService.get().startTunnel(it, true) Action.START.name -> tunnelService.get().startTunnel(it)
Action.STOP.name -> tunnelService.get().stopTunnel() Action.STOP.name -> tunnelService.get().stopTunnel()
else -> Unit else -> Unit
} }

View File

@ -66,7 +66,7 @@ class TunnelControlTile : TileService() {
applicationScope.launch { applicationScope.launch {
if (tunnelService.vpnState.value.status.isUp()) return@launch tunnelService.stopTunnel() if (tunnelService.vpnState.value.status.isUp()) return@launch tunnelService.stopTunnel()
appDataRepository.getStartTunnelConfig()?.let { appDataRepository.getStartTunnelConfig()?.let {
tunnelService.startTunnel(it, true) tunnelService.startTunnel(it)
} }
} }
} }

View File

@ -6,7 +6,7 @@ import kotlinx.coroutines.flow.StateFlow
interface TunnelService : Tunnel, org.amnezia.awg.backend.Tunnel { interface TunnelService : Tunnel, org.amnezia.awg.backend.Tunnel {
suspend fun startTunnel(tunnelConfig: TunnelConfig?, background: Boolean = false) suspend fun startTunnel(tunnelConfig: TunnelConfig?)
suspend fun stopTunnel() suspend fun stopTunnel()

View File

@ -2,33 +2,44 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel
import com.wireguard.android.backend.Backend import com.wireguard.android.backend.Backend
import com.wireguard.android.backend.Tunnel.State import com.wireguard.android.backend.Tunnel.State
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.module.Ethernet
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.module.Kernel import com.zaneschepke.wireguardautotunnel.module.Kernel
import com.zaneschepke.wireguardautotunnel.module.MobileData
import com.zaneschepke.wireguardautotunnel.module.Wifi
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationAction import com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model.NetworkState
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService.Companion.VPN_NOTIFICATION_ID import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService.Companion.VPN_NOTIFICATION_ID
import com.zaneschepke.wireguardautotunnel.service.notification.WireGuardNotification
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.AmneziaStatistics import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.AmneziaStatistics
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.WireGuardStatistics import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.WireGuardStatistics
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.service.notification.WireGuardNotification
import com.zaneschepke.wireguardautotunnel.util.StringValue
import com.zaneschepke.wireguardautotunnel.util.extensions.asAmBackendState import com.zaneschepke.wireguardautotunnel.util.extensions.asAmBackendState
import com.zaneschepke.wireguardautotunnel.util.extensions.asBackendState import com.zaneschepke.wireguardautotunnel.util.extensions.asBackendState
import com.zaneschepke.wireguardautotunnel.util.extensions.cancelWithMessage import com.zaneschepke.wireguardautotunnel.util.extensions.cancelWithMessage
import com.zaneschepke.wireguardautotunnel.util.extensions.isReachable import com.zaneschepke.wireguardautotunnel.util.extensions.isReachable
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
@ -37,6 +48,7 @@ import kotlinx.coroutines.withContext
import org.amnezia.awg.backend.Tunnel import org.amnezia.awg.backend.Tunnel
import timber.log.Timber import timber.log.Timber
import java.net.InetAddress import java.net.InetAddress
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider import javax.inject.Provider
@ -51,6 +63,9 @@ constructor(
@IoDispatcher private val ioDispatcher: CoroutineDispatcher, @IoDispatcher private val ioDispatcher: CoroutineDispatcher,
private val serviceManager: ServiceManager, private val serviceManager: ServiceManager,
private val notificationService: NotificationService, private val notificationService: NotificationService,
@Wifi private val wifiService: NetworkService,
@MobileData private val mobileDataService: NetworkService,
@Ethernet private val ethernetService: NetworkService,
) : TunnelService { ) : TunnelService {
private val _vpnState = MutableStateFlow(VpnState()) private val _vpnState = MutableStateFlow(VpnState())
@ -59,9 +74,11 @@ constructor(
private var statsJob: Job? = null private var statsJob: Job? = null
private var tunnelChangesJob: Job? = null private var tunnelChangesJob: Job? = null
private var pingJob: Job? = null private var pingJob: Job? = null
private var networkJob: Job? = null
@get:Synchronized @set:Synchronized @get:Synchronized @set:Synchronized
private var isKernelBackend: Boolean? = null private var isKernelBackend: Boolean? = null
private val isNetworkAvailable = AtomicBoolean(false)
private val tunnelControlMutex = Mutex() private val tunnelControlMutex = Mutex()
@ -88,6 +105,23 @@ constructor(
} }
} }
// TODO refactor duplicate
@OptIn(FlowPreview::class)
private fun combineNetworkEventsJob(): Flow<NetworkState> {
return combine(
wifiService.status,
mobileDataService.status,
ethernetService.status,
) { wifi, mobileData, ethernet ->
NetworkState(
wifi.available,
mobileData.available,
ethernet.available,
wifi.name,
)
}.distinctUntilChanged()
}
private suspend fun setState(tunnelConfig: TunnelConfig, tunnelState: TunnelState): Result<TunnelState> { private suspend fun setState(tunnelConfig: TunnelConfig, tunnelState: TunnelState): Result<TunnelState> {
return runCatching { return runCatching {
when (val backend = backend()) { when (val backend = backend()) {
@ -113,20 +147,43 @@ constructor(
} }
} }
override suspend fun startTunnel(tunnelConfig: TunnelConfig?, background: Boolean) { override suspend fun startTunnel(tunnelConfig: TunnelConfig?) {
withContext(ioDispatcher) { withContext(ioDispatcher) {
if (tunnelConfig == null || isTunnelAlreadyRunning(tunnelConfig)) return@withContext if (tunnelConfig == null || isTunnelAlreadyRunning(tunnelConfig)) return@withContext
onBeforeStart(background) onBeforeStart(tunnelConfig)
updateTunnelConfig(tunnelConfig) // need to update this here updateTunnelConfig(tunnelConfig) // need to update this here
appDataRepository.tunnels.save(tunnelConfig.copy(isActive = true))
withServiceActive { withServiceActive {
setState(tunnelConfig, TunnelState.UP).onSuccess { setState(tunnelConfig, TunnelState.UP).onSuccess {
updateTunnelState(it, tunnelConfig) updateTunnelState(it, tunnelConfig)
onTunnelStart(tunnelConfig, background) startActiveTunnelJobs()
}.onFailure {
onTunnelStop(tunnelConfig)
// TODO improve this with better statuses and handling
showTunnelStartFailed()
} }
} }
} }
} }
private fun showTunnelStartFailed() {
if (WireGuardAutoTunnel.isForeground()) {
SnackbarController.showMessage(StringValue.StringResource(R.string.error_tunnel_start))
} else {
launchStartFailedNotification()
}
}
private fun launchStartFailedNotification() {
with(notificationService) {
val notification = createNotification(
WireGuardNotification.NotificationChannels.VPN,
title = context.getString(R.string.error_tunnel_start),
)
show(VPN_NOTIFICATION_ID, notification)
}
}
override suspend fun stopTunnel() { override suspend fun stopTunnel() {
withContext(ioDispatcher) { withContext(ioDispatcher) {
if (_vpnState.value.status.isDown()) return@withContext if (_vpnState.value.status.isDown()) return@withContext
@ -200,31 +257,10 @@ constructor(
} }
} }
private suspend fun onBeforeStart(background: Boolean) { private suspend fun onBeforeStart(tunnelConfig: TunnelConfig) {
with(_vpnState.value) { with(_vpnState.value) {
if (status.isUp()) stopTunnel() else clearJobsAndStats() if (status.isUp()) stopTunnel() else clearJobsAndStats()
if (isKernelBackend == true || background) serviceManager.startBackgroundService(tunnelConfig) serviceManager.startBackgroundService(tunnelConfig)
}
}
private suspend fun onTunnelStart(tunnelConfig: TunnelConfig, background: Boolean) {
startActiveTunnelJobs()
if (_vpnState.value.status.isUp()) {
appDataRepository.tunnels.save(tunnelConfig.copy(isActive = true))
}
if (isKernelBackend == false && !background) launchUserspaceTunnelNotification()
}
private fun launchUserspaceTunnelNotification() {
with(notificationService) {
val notification = createNotification(
WireGuardNotification.NotificationChannels.VPN,
title = "${context.getString(R.string.tunnel_running)} - ${_vpnState.value.tunnelConfig?.name}",
actions = listOf(
notificationService.createNotificationAction(NotificationAction.TUNNEL_OFF),
),
)
show(VPN_NOTIFICATION_ID, notification)
} }
} }
@ -278,14 +314,21 @@ constructor(
statsJob?.cancelWithMessage("Tunnel stats job cancelled") statsJob?.cancelWithMessage("Tunnel stats job cancelled")
tunnelChangesJob?.cancelWithMessage("Tunnel changes job cancelled") tunnelChangesJob?.cancelWithMessage("Tunnel changes job cancelled")
pingJob?.cancelWithMessage("Ping job cancelled") pingJob?.cancelWithMessage("Ping job cancelled")
networkJob?.cancelWithMessage("Network job cancelled")
} }
override fun startActiveTunnelJobs() { override fun startActiveTunnelJobs() {
statsJob = startTunnelStatisticsJob() statsJob = startTunnelStatisticsJob()
tunnelChangesJob = startTunnelConfigChangesJob() tunnelChangesJob = startTunnelConfigChangesJob()
if (_vpnState.value.tunnelConfig?.isPingEnabled == true) pingJob = startPingJob() if (_vpnState.value.tunnelConfig?.isPingEnabled == true) {
startPingJobs()
}
} }
private fun startPingJobs() {
pingJob = startPingJob()
networkJob = startNetworkJob()
}
override fun getName(): String { override fun getName(): String {
return _vpnState.value.tunnelConfig?.name ?: "" return _vpnState.value.tunnelConfig?.name ?: ""
} }
@ -331,6 +374,7 @@ constructor(
if (this == null) return if (this == null) return
if (!isPingEnabled && pingJob?.isActive == true) { if (!isPingEnabled && pingJob?.isActive == true) {
pingJob?.cancelWithMessage("Ping job cancelled") pingJob?.cancelWithMessage("Ping job cancelled")
networkJob?.cancelWithMessage("Network job cancelled")
return return
} }
restartPingJob() restartPingJob()
@ -339,7 +383,8 @@ constructor(
private fun restartPingJob() { private fun restartPingJob() {
pingJob?.cancelWithMessage("Ping job cancelled") pingJob?.cancelWithMessage("Ping job cancelled")
pingJob = startPingJob() networkJob?.cancelWithMessage("Network job cancelled")
startPingJobs()
} }
private fun startTunnelConfigChangesJob() = applicationScope.launch(ioDispatcher) { private fun startTunnelConfigChangesJob() = applicationScope.launch(ioDispatcher) {
@ -376,13 +421,16 @@ constructor(
do { do {
run { run {
with(_vpnState.value) { with(_vpnState.value) {
// TODO ignore when no connectivity if (status.isUp() && tunnelConfig != null && isNetworkAvailable.get()) {
if (status.isUp() && tunnelConfig != null) {
val reachable = pingTunnel(tunnelConfig) val reachable = pingTunnel(tunnelConfig)
if (reachable.contains(false)) { if (reachable.contains(false)) {
if (isNetworkAvailable.get()) {
Timber.i("Ping result: target was not reachable, bouncing the tunnel") Timber.i("Ping result: target was not reachable, bouncing the tunnel")
bounceTunnel() bounceTunnel()
delay(tunnelConfig.pingCooldown ?: Constants.PING_COOLDOWN) delay(tunnelConfig.pingCooldown ?: Constants.PING_COOLDOWN)
} else {
Timber.i("Ping result: target was not reachable, but not network available")
}
return@run return@run
} else { } else {
Timber.i("Ping result: all ping targets were reached successfully") Timber.i("Ping result: all ping targets were reached successfully")
@ -394,6 +442,17 @@ constructor(
} while (true) } while (true)
} }
private fun startNetworkJob() = applicationScope.launch(ioDispatcher) {
combineNetworkEventsJob().collect {
Timber.d("New network state: $it")
if (!it.isWifiConnected && !it.isEthernetConnected && !it.isMobileDataConnected) {
isNetworkAvailable.set(false)
} else {
isNetworkAvailable.set(true)
}
}
}
override fun onStateChange(newState: Tunnel.State) { override fun onStateChange(newState: Tunnel.State) {
_vpnState.update { _vpnState.update {
it.copy(status = TunnelState.from(newState)) it.copy(status = TunnelState.from(newState))

View File

@ -112,6 +112,7 @@ constructor(
} }
private suspend fun initTunnel() { private suspend fun initTunnel() {
withContext(ioDispatcher) {
if (tunnelService.get().getState() == TunnelState.UP) tunnelService.get().startActiveTunnelJobs() if (tunnelService.get().getState() == TunnelState.UP) tunnelService.get().startActiveTunnelJobs()
val activeTunnels = appDataRepository.tunnels.getActive() val activeTunnels = appDataRepository.tunnels.getActive()
if (activeTunnels.isNotEmpty() && if (activeTunnels.isNotEmpty() &&
@ -120,6 +121,7 @@ constructor(
tunnelService.get().startTunnel(activeTunnels.first()) tunnelService.get().startTunnel(activeTunnels.first())
} }
} }
}
private suspend fun initPin() { private suspend fun initPin() {
val isPinEnabled = appDataRepository.appState.isPinLockEnabled() val isPinEnabled = appDataRepository.appState.isPinLockEnabled()

View File

@ -13,7 +13,6 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
@ -142,7 +141,6 @@ class MainActivity : AppCompatActivity() {
SnackbarControllerProvider { host -> SnackbarControllerProvider { host ->
WireguardAutoTunnelTheme(theme = appUiState.generalState.theme) { WireguardAutoTunnelTheme(theme = appUiState.generalState.theme) {
Scaffold( Scaffold(
modifier = Modifier.background(color = MaterialTheme.colorScheme.background),
contentWindowInsets = WindowInsets(0), contentWindowInsets = WindowInsets(0),
snackbarHost = { snackbarHost = {
SnackbarHost(host) { snackbarData: SnackbarData -> SnackbarHost(host) { snackbarData: SnackbarData ->

View File

@ -82,7 +82,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
val startAutoTunnel = withVpnPermission<Unit> { viewModel.onToggleAutoTunnel() } val startAutoTunnel = withVpnPermission<Unit> { viewModel.onToggleAutoTunnel() }
val startTunnel = withVpnPermission<TunnelConfig> { val startTunnel = withVpnPermission<TunnelConfig> {
viewModel.onTunnelStart(it, uiState.settings.isKernelEnabled) viewModel.onTunnelStart(it)
} }
val autoTunnelToggleBattery = withIgnoreBatteryOpt(uiState.generalState.isBatteryOptimizationDisableShown) { val autoTunnelToggleBattery = withIgnoreBatteryOpt(uiState.generalState.isBatteryOptimizationDisableShown) {
if (!uiState.generalState.isBatteryOptimizationDisableShown) viewModel.setBatteryOptimizeDisableShown() if (!uiState.generalState.isBatteryOptimizationDisableShown) viewModel.setBatteryOptimizeDisableShown()
@ -129,7 +129,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) { fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) {
if (!checked) viewModel.onTunnelStop().also { return } if (!checked) viewModel.onTunnelStop().also { return }
if (uiState.settings.isKernelEnabled) { if (uiState.settings.isKernelEnabled) {
viewModel.onTunnelStart(tunnel, uiState.settings.isKernelEnabled) viewModel.onTunnelStart(tunnel)
} else { } else {
startTunnel.invoke(tunnel) startTunnel.invoke(tunnel)
} }
@ -226,8 +226,9 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
) { tunnel -> ) { tunnel ->
val expanded = uiState.generalState.isTunnelStatsExpanded val expanded = uiState.generalState.isTunnelStatsExpanded
TunnelRowItem( TunnelRowItem(
tunnel.id == uiState.vpnState.tunnelConfig?.id && tunnel.id == uiState.vpnState.tunnelConfig?.id && (
uiState.vpnState.status.isUp(), uiState.vpnState.status.isUp() || (uiState.settings.isKernelEnabled && tunnel.isActive)
),
expanded, expanded,
selectedTunnel?.id == tunnel.id, selectedTunnel?.id == tunnel.id,
tunnel, tunnel,

View File

@ -67,9 +67,9 @@ constructor(
appDataRepository.appState.setTunnelStatsExpanded(expanded) appDataRepository.appState.setTunnelStatsExpanded(expanded)
} }
fun onTunnelStart(tunnelConfig: TunnelConfig, background: Boolean) = viewModelScope.launch { fun onTunnelStart(tunnelConfig: TunnelConfig) = viewModelScope.launch {
Timber.i("Starting tunnel ${tunnelConfig.name}") Timber.i("Starting tunnel ${tunnelConfig.name}")
tunnelService.get().startTunnel(tunnelConfig, background) tunnelService.get().startTunnel(tunnelConfig)
} }
fun onTunnelStop() = viewModelScope.launch { fun onTunnelStop() = viewModelScope.launch {
@ -86,20 +86,6 @@ constructor(
} }
} }
private fun generateQrCodeTunnelName(config: String): String {
var defaultName = generateQrCodeDefaultName(config)
val lines = config.lines().toMutableList()
val linesIterator = lines.iterator()
while (linesIterator.hasNext()) {
val next = linesIterator.next()
if (next.contains(Constants.QR_CODE_NAME_PROPERTY)) {
defaultName = next.substringAfter(Constants.QR_CODE_NAME_PROPERTY).trim()
break
}
}
return defaultName
}
private suspend fun makeTunnelNameUnique(name: String): String { private suspend fun makeTunnelNameUnique(name: String): String {
return withContext(ioDispatcher) { return withContext(ioDispatcher) {
val tunnels = appDataRepository.tunnels.getAll() val tunnels = appDataRepository.tunnels.getAll()

View File

@ -202,4 +202,5 @@
<string name="remove_amnezia_compatibility">Remove Amnezia compatibility</string> <string name="remove_amnezia_compatibility">Remove Amnezia compatibility</string>
<string name="exclude_lan">Exclude LAN</string> <string name="exclude_lan">Exclude LAN</string>
<string name="include_lan">Include LAN</string> <string name="include_lan">Include LAN</string>
<string name="error_tunnel_start">Failed to starting tunnel</string>
</resources> </resources>

View File

@ -0,0 +1,6 @@
What's new:
- Ping feature now works independent of auto tunnel
- Added convenience action for Amnezia compatibility
- Added convenience action for excluding LAN from tunnel
- Added debounce delay tuning option for auto tunnel
- Many bug fixes and improvements