feat!: move ping from auto tunnel to tunnel

Ping feature will not be tunnel specific and work without auto tunneling being active
This commit is contained in:
Zane Schepke 2025-01-01 18:16:26 -05:00
parent ba064b267f
commit f772dc0f8a
14 changed files with 245 additions and 244 deletions

View File

@ -66,7 +66,7 @@ data class TunnelConfig(
) {
fun toAmConfig(): org.amnezia.awg.config.Config {
return configFromAmQuick(if (amQuick != "") amQuick else wgQuick)
return configFromAmQuick(if (amQuick.isNotBlank()) amQuick else wgQuick)
}
fun toWgConfig(): Config {

View File

@ -9,7 +9,6 @@ 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
import com.zaneschepke.wireguardautotunnel.module.Ethernet
@ -29,29 +28,20 @@ import com.zaneschepke.wireguardautotunnel.service.notification.WireGuardNotific
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
import com.zaneschepke.wireguardautotunnel.util.extensions.cancelWithMessage
import com.zaneschepke.wireguardautotunnel.util.extensions.getCurrentWifiName
import com.zaneschepke.wireguardautotunnel.util.extensions.isReachable
import com.zaneschepke.wireguardautotunnel.util.extensions.onNotRunning
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
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
@ -100,10 +90,6 @@ class AutoTunnelService : LifecycleService() {
private var wakeLock: PowerManager.WakeLock? = null
private val pingTunnelRestartActive = AtomicBoolean(false)
private var pingJob: Job? = null
override fun onCreate() {
super.onCreate()
serviceManager.autoTunnelService.complete(this)
@ -135,7 +121,6 @@ class AutoTunnelService : LifecycleService() {
}
startAutoTunnelJob()
startAutoTunnelStateJob()
startPingStateJob()
}.onFailure {
Timber.e(it)
}
@ -147,7 +132,6 @@ class AutoTunnelService : LifecycleService() {
}
override fun onDestroy() {
cancelAndResetPingJob()
serviceManager.autoTunnelService = CompletableDeferred()
super.onDestroy()
}
@ -184,59 +168,6 @@ class AutoTunnelService : LifecycleService() {
}
}
private fun startPingJob() = lifecycleScope.launch {
watchForPingFailure()
}
private fun startPingStateJob() = lifecycleScope.launch {
autoTunnelStateFlow.collect {
if (it == defaultState) return@collect
if (it.isPingEnabled()) {
pingJob.onNotRunning { pingJob = startPingJob() }
} else {
if (!pingTunnelRestartActive.get()) cancelAndResetPingJob()
}
}
}
private suspend fun watchForPingFailure() {
withContext(ioDispatcher) {
Timber.i("Starting ping watcher")
runCatching {
do {
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) {
Timber.d("Pinging custom ip : ${vpnState.tunnelConfig.pingIp}")
listOf(InetAddress.getByName(vpnState.tunnelConfig.pingIp).isReachable(Constants.PING_TIMEOUT.toInt()))
} else {
Timber.d("Pinging all peers")
config.peers.map { peer ->
peer.isReachable()
}
}
Timber.i("Ping results reachable: $results")
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
}
}
}
delay(vpnState.tunnelConfig?.pingInterval ?: Constants.PING_INTERVAL)
} while (true)
}.onFailure {
Timber.e(it)
}
}
}
private fun startAutoTunnelStateJob() = lifecycleScope.launch(ioDispatcher) {
combine(
combineSettings(),
@ -263,11 +194,6 @@ class AutoTunnelService : LifecycleService() {
}
}
private fun cancelAndResetPingJob() {
pingJob?.cancelWithMessage("Ping job canceled")
pingJob = null
}
@OptIn(FlowPreview::class)
private fun combineNetworkEventsJob(): Flow<NetworkState> {
return combine(
@ -281,7 +207,7 @@ class AutoTunnelService : LifecycleService() {
wifi.name,
wifi.capabilities,
)
}.distinctUntilChanged().filterNot { it.isWifiConnected && it.wifiName == null }
}.distinctUntilChanged()
}
private fun combineSettings(): Flow<Pair<Settings, TunnelConfigs>> {

View File

@ -17,8 +17,11 @@ import com.zaneschepke.wireguardautotunnel.service.notification.WireGuardNotific
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.AmneziaStatistics
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.WireGuardStatistics
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.asAmBackendState
import com.zaneschepke.wireguardautotunnel.util.extensions.asBackendState
import com.zaneschepke.wireguardautotunnel.util.extensions.cancelWithMessage
import com.zaneschepke.wireguardautotunnel.util.extensions.isReachable
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
@ -33,6 +36,7 @@ import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.amnezia.awg.backend.Tunnel
import timber.log.Timber
import java.net.InetAddress
import javax.inject.Inject
import javax.inject.Provider
@ -54,6 +58,7 @@ constructor(
private var statsJob: Job? = null
private var tunnelChangesJob: Job? = null
private var pingJob: Job? = null
@get:Synchronized @set:Synchronized
private var isKernelBackend: Boolean? = null
@ -86,16 +91,9 @@ constructor(
private suspend fun setState(tunnelConfig: TunnelConfig, tunnelState: TunnelState): Result<TunnelState> {
return runCatching {
when (val backend = backend()) {
is Backend -> backend.setState(this, tunnelState.toWgState(), TunnelConfig.configFromWgQuick(tunnelConfig.wgQuick)).let { TunnelState.from(it) }
is Backend -> backend.setState(this, tunnelState.toWgState(), tunnelConfig.toWgConfig()).let { TunnelState.from(it) }
is org.amnezia.awg.backend.Backend -> {
val config = if (tunnelConfig.amQuick.isBlank()) {
TunnelConfig.configFromAmQuick(
tunnelConfig.wgQuick,
)
} else {
TunnelConfig.configFromAmQuick(tunnelConfig.amQuick)
}
backend.setState(this, tunnelState.toAmState(), config).let {
backend.setState(this, tunnelState.toAmState(), tunnelConfig.toAmConfig()).let {
TunnelState.from(it)
}
}
@ -108,9 +106,11 @@ constructor(
}
private fun isTunnelAlreadyRunning(tunnelConfig: TunnelConfig): Boolean {
val isRunning = tunnelConfig.id == _vpnState.value.tunnelConfig?.id && _vpnState.value.status.isUp()
if (isRunning) Timber.w("Tunnel already running")
return isRunning
return with(_vpnState.value) {
this.tunnelConfig?.id == tunnelConfig.id && status.isUp().also {
if (it) Timber.w("Tunnel already running")
}
}
}
override suspend fun startTunnel(tunnelConfig: TunnelConfig?, background: Boolean) {
@ -165,10 +165,12 @@ constructor(
}
override suspend fun bounceTunnel() {
_vpnState.value.tunnelConfig?.let {
withServiceActive {
toggleTunnel(it)
toggleTunnel(it)
with(_vpnState.value) {
if (tunnelConfig != null && status.isUp()) {
withServiceActive {
toggleTunnel(tunnelConfig)
toggleTunnel(tunnelConfig)
}
}
}
}
@ -273,13 +275,15 @@ constructor(
}
override fun cancelActiveTunnelJobs() {
statsJob?.cancel()
tunnelChangesJob?.cancel()
statsJob?.cancelWithMessage("Tunnel stats job cancelled")
tunnelChangesJob?.cancelWithMessage("Tunnel changes job cancelled")
pingJob?.cancelWithMessage("Ping job cancelled")
}
override fun startActiveTunnelJobs() {
statsJob = startTunnelStatisticsJob()
tunnelChangesJob = startTunnelConfigChangesJob()
if (_vpnState.value.tunnelConfig?.isPingEnabled == true) pingJob = startPingJob()
}
override fun getName(): String {
@ -304,22 +308,92 @@ constructor(
}
}
private fun isQuickConfigChanged(config: TunnelConfig): Boolean {
return with(_vpnState.value) {
if (tunnelConfig == null) return false
config.wgQuick != tunnelConfig.wgQuick ||
config.amQuick != tunnelConfig.amQuick
}
}
private fun isPingConfigMatching(config: TunnelConfig): Boolean {
return with(_vpnState.value.tunnelConfig) {
if (this == null) return true
config.isPingEnabled == isPingEnabled &&
pingIp == config.pingIp &&
config.pingCooldown == pingCooldown &&
config.pingInterval == pingInterval
}
}
private fun handlePingConfigChanges() {
with(_vpnState.value.tunnelConfig) {
if (this == null) return
if (!isPingEnabled && pingJob?.isActive == true) {
pingJob?.cancelWithMessage("Ping job cancelled")
return
}
restartPingJob()
}
}
private fun restartPingJob() {
pingJob?.cancelWithMessage("Ping job cancelled")
pingJob = startPingJob()
}
private fun startTunnelConfigChangesJob() = applicationScope.launch(ioDispatcher) {
tunnelConfigRepository.getTunnelConfigsFlow().collect {
tunnelConfigRepository.getTunnelConfigsFlow().collect { tunnels ->
with(_vpnState.value) {
if (status.isDown() || tunnelConfig == null) return@collect
val vpnConfigFromStorage = it.first { it.id == tunnelConfig.id }
val isRestartNeeded = vpnConfigFromStorage.wgQuick != tunnelConfig.wgQuick ||
vpnConfigFromStorage.amQuick != tunnelConfig.amQuick
updateTunnelConfig(vpnConfigFromStorage)
if (isRestartNeeded) {
Timber.d("Bouncing tunnel on config change")
bounceTunnel()
if (tunnelConfig == null) return@collect
val storageConfig = tunnels.firstOrNull { it.id == tunnelConfig.id }
if (storageConfig == null) return@collect
val quickChanged = isQuickConfigChanged(storageConfig)
val pingMatching = isPingConfigMatching(storageConfig)
updateTunnelConfig(storageConfig)
if (quickChanged) bounceTunnel()
if (!pingMatching) handlePingConfigChanges()
}
}
}
private suspend fun pingTunnel(tunnelConfig: TunnelConfig): List<Boolean> {
return withContext(ioDispatcher) {
val config = tunnelConfig.toWgConfig()
if (tunnelConfig.pingIp != null) {
Timber.i("Pinging custom ip")
listOf(InetAddress.getByName(tunnelConfig.pingIp).isReachable(Constants.PING_TIMEOUT.toInt()))
} else {
Timber.i("Pinging all peers")
config.peers.map { peer ->
peer.isReachable()
}
}
}
}
private fun startPingJob() = applicationScope.launch(ioDispatcher) {
do {
run {
with(_vpnState.value) {
// TODO ignore when no connectivity
if (status.isUp() && tunnelConfig != null) {
val reachable = pingTunnel(tunnelConfig)
if (reachable.contains(false)) {
Timber.i("Ping result: target was not reachable, bouncing the tunnel")
bounceTunnel()
delay(tunnelConfig.pingCooldown ?: Constants.PING_COOLDOWN)
return@run
} else {
Timber.i("Ping result: all ping targets were reached successfully")
}
}
delay(tunnelConfig?.pingInterval ?: Constants.PING_INTERVAL)
}
}
} while (true)
}
override fun onStateChange(newState: Tunnel.State) {
_vpnState.update {
it.copy(status = TunnelState.from(newState))

View File

@ -381,15 +381,6 @@ constructor(
serviceManager.startAutoTunnel(true)
}
fun onTogglePrimaryTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch {
appDataRepository.tunnels.updatePrimaryTunnel(
when (tunnelConfig.isPrimaryTunnel) {
true -> null
false -> tunnelConfig
},
)
}
private suspend fun rebuildConfigs(
amConfig: org.amnezia.awg.config.Config,
wgConfig: Config,

View File

@ -5,6 +5,7 @@ import android.graphics.Color.TRANSPARENT
import android.os.Build
import android.os.Bundle
import androidx.activity.SystemBarStyle
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
@ -35,6 +36,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
@ -69,6 +71,7 @@ import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlin.system.exitProcess
@ -228,7 +231,7 @@ class MainActivity : AppCompatActivity() {
composable<Route.TunnelOptions> {
val args = it.toRoute<Route.TunnelOptions>()
val config = appUiState.tunnels.first { it.id == args.id }
OptionsScreen(config, viewModel)
OptionsScreen(config)
}
composable<Route.Lock> {
PinLockScreen(viewModel)
@ -250,6 +253,13 @@ class MainActivity : AppCompatActivity() {
TunnelAutoTunnelScreen(config, appUiState.settings)
}
}
BackHandler(enabled = true) {
lifecycleScope.launch {
if (!navController.popBackStack()) {
this@MainActivity.finish()
}
}
}
}
}
}
@ -257,9 +267,4 @@ class MainActivity : AppCompatActivity() {
}
}
}
override fun onDestroy() {
super.onDestroy()
// save battery by not polling stats while app is closed
tunnelService.cancelActiveTunnelJobs()
}
}

View File

@ -19,7 +19,9 @@ fun TopNavBar(title: String, trailing: @Composable () -> Unit = {}, showBack: Bo
},
navigationIcon = {
if (showBack) {
IconButton(onClick = { navController.popBackStack() }) {
IconButton(onClick = {
navController.popBackStack()
}) {
val icon = Icons.AutoMirrored.Outlined.ArrowBack
Icon(
imageVector = icon,

View File

@ -17,7 +17,6 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.AirplanemodeActive
import androidx.compose.material.icons.outlined.Code
import androidx.compose.material.icons.outlined.Filter1
import androidx.compose.material.icons.outlined.NetworkPing
import androidx.compose.material.icons.outlined.Security
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.SettingsEthernet
@ -316,24 +315,6 @@ fun AutoTunnelScreen(uiState: AppUiState, viewModel: AutoTunnelViewModel = hiltV
viewModel.onToggleTunnelOnEthernet()
},
),
SelectionItem(
Icons.Outlined.NetworkPing,
title = {
Text(
stringResource(R.string.restart_on_ping),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = {
ScaledSwitch(
checked = uiState.settings.isPingEnabled,
onClick = { viewModel.onToggleRestartOnPing() },
)
},
onClick = {
viewModel.onToggleRestartOnPing()
},
),
SelectionItem(
Icons.Outlined.AirplanemodeActive,
title = {

View File

@ -119,16 +119,6 @@ constructor(
}
}
fun onToggleRestartOnPing() = viewModelScope.launch {
with(settings.value) {
appDataRepository.settings.save(
copy(
isPingEnabled = !isPingEnabled,
),
)
}
}
fun onToggleStopOnNoInternet() = viewModelScope.launch {
with(settings.value) {
appDataRepository.settings.save(

View File

@ -6,11 +6,13 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.CallSplit
import androidx.compose.material.icons.outlined.Bolt
import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material.icons.outlined.NetworkPing
import androidx.compose.material.icons.outlined.Star
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
@ -24,22 +26,30 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.ui.AppViewModel
import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.config.SubmitConfigurationTextBox
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.ForwardButton
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.isValidIpv4orIpv6Address
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
import kotlin.text.isBlank
import kotlin.text.isNullOrBlank
import kotlin.text.toLong
@Composable
fun OptionsScreen(tunnelConfig: TunnelConfig, appViewModel: AppViewModel) {
fun OptionsScreen(tunnelConfig: TunnelConfig, viewModel: TunnelOptionsViewModel = hiltViewModel()) {
val navController = LocalNavController.current
var currentText by remember { mutableStateOf("") }
@ -83,10 +93,10 @@ fun OptionsScreen(tunnelConfig: TunnelConfig, appViewModel: AppViewModel) {
trailing = {
ScaledSwitch(
tunnelConfig.isPrimaryTunnel,
onClick = { appViewModel.onTogglePrimaryTunnel(tunnelConfig) },
onClick = { viewModel.onTogglePrimaryTunnel(tunnelConfig) },
)
},
onClick = { appViewModel.onTogglePrimaryTunnel(tunnelConfig) },
onClick = { viewModel.onTogglePrimaryTunnel(tunnelConfig) },
),
SelectionItem(
Icons.Outlined.Bolt,
@ -141,6 +151,81 @@ fun OptionsScreen(tunnelConfig: TunnelConfig, appViewModel: AppViewModel) {
),
),
)
SurfaceSelectionGroupButton(
buildList {
add(
SelectionItem(
Icons.Outlined.NetworkPing,
title = {
Text(
stringResource(R.string.restart_on_ping),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = {
ScaledSwitch(
checked = tunnelConfig.isPingEnabled,
onClick = { viewModel.onToggleRestartOnPing(tunnelConfig) },
)
},
onClick = { viewModel.onToggleRestartOnPing(tunnelConfig) },
),
)
if (tunnelConfig.isPingEnabled) {
add(
SelectionItem(
title = {},
description = {
SubmitConfigurationTextBox(
tunnelConfig.pingIp,
stringResource(R.string.set_custom_ping_ip),
stringResource(R.string.default_ping_ip),
isErrorValue = { !it.isNullOrBlank() && !it.isValidIpv4orIpv6Address() },
onSubmit = {
viewModel.saveTunnelChanges(
tunnelConfig.copy(pingIp = it.ifBlank { null }),
)
},
)
fun isSecondsError(seconds: String?): Boolean {
return seconds?.let { value -> if (value.isBlank()) false else value.toLong() >= Long.MAX_VALUE / 1000 }
?: false
}
SubmitConfigurationTextBox(
tunnelConfig.pingInterval?.let { (it / 1000).toString() },
stringResource(R.string.set_custom_ping_internal),
"(${stringResource(R.string.optional_default)} ${Constants.PING_INTERVAL / 1000})",
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Done,
),
isErrorValue = ::isSecondsError,
onSubmit = {
viewModel.saveTunnelChanges(
tunnelConfig.copy(pingInterval = if (it.isBlank()) null else it.toLong() * 1000),
)
},
)
SubmitConfigurationTextBox(
tunnelConfig.pingCooldown?.let { (it / 1000).toString() },
stringResource(R.string.set_custom_ping_cooldown),
"(${stringResource(R.string.optional_default)} ${Constants.PING_COOLDOWN / 1000})",
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
),
isErrorValue = ::isSecondsError,
onSubmit = {
viewModel.saveTunnelChanges(
tunnelConfig.copy(pingCooldown = if (it.isBlank()) null else it.toLong() * 1000),
)
},
)
},
),
)
}
},
)
}
}
}

View File

@ -0,0 +1,37 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.tunneloptions
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class TunnelOptionsViewModel
@Inject
constructor(
private val appDataRepository: AppDataRepository,
) : ViewModel() {
fun onToggleRestartOnPing(tunnelConfig: TunnelConfig) = viewModelScope.launch {
appDataRepository.tunnels.save(
tunnelConfig.copy(
isPingEnabled = !tunnelConfig.isPingEnabled,
),
)
}
fun onTogglePrimaryTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch {
appDataRepository.tunnels.updatePrimaryTunnel(
when (tunnelConfig.isPrimaryTunnel) {
true -> null
false -> tunnelConfig
},
)
}
fun saveTunnelChanges(tunnelConfig: TunnelConfig) = viewModelScope.launch {
appDataRepository.tunnels.save(tunnelConfig)
}
}

View File

@ -8,10 +8,8 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.NetworkPing
import androidx.compose.material.icons.outlined.PhoneAndroid
import androidx.compose.material.icons.outlined.Security
import androidx.compose.material.icons.outlined.SettingsEthernet
@ -28,8 +26,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.zaneschepke.wireguardautotunnel.R
@ -38,13 +34,10 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.ui.common.config.SubmitConfigurationTextBox
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components.TrustedNetworkTextBox
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.autotunnel.components.WildcardsLabel
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.isValidIpv4orIpv6Address
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledWidth
@ -119,76 +112,8 @@ fun TunnelAutoTunnelScreen(tunnelConfig: TunnelConfig, settings: Settings, tunne
},
onClick = { tunnelAutoTunnelViewModel.onToggleIsEthernetTunnel(tunnelConfig) },
),
SelectionItem(
Icons.Outlined.NetworkPing,
title = {
Text(
stringResource(R.string.restart_on_ping),
style = MaterialTheme.typography.bodyMedium.copy(MaterialTheme.colorScheme.onSurface),
)
},
trailing = {
ScaledSwitch(
checked = tunnelConfig.isPingEnabled,
onClick = { tunnelAutoTunnelViewModel.onToggleRestartOnPing(tunnelConfig) },
)
},
onClick = { tunnelAutoTunnelViewModel.onToggleRestartOnPing(tunnelConfig) },
),
),
)
if (tunnelConfig.isPingEnabled || settings.isPingEnabled) {
add(
SelectionItem(
title = {},
description = {
SubmitConfigurationTextBox(
tunnelConfig.pingIp,
stringResource(R.string.set_custom_ping_ip),
stringResource(R.string.default_ping_ip),
isErrorValue = { !it.isNullOrBlank() && !it.isValidIpv4orIpv6Address() },
onSubmit = {
tunnelAutoTunnelViewModel.saveTunnelChanges(
tunnelConfig.copy(pingIp = it.ifBlank { null }),
)
},
)
fun isSecondsError(seconds: String?): Boolean {
return seconds?.let { value -> if (value.isBlank()) false else value.toLong() >= Long.MAX_VALUE / 1000 } ?: false
}
SubmitConfigurationTextBox(
tunnelConfig.pingInterval?.let { (it / 1000).toString() },
stringResource(R.string.set_custom_ping_internal),
"(${stringResource(R.string.optional_default)} ${Constants.PING_INTERVAL / 1000})",
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Done,
),
isErrorValue = ::isSecondsError,
onSubmit = {
tunnelAutoTunnelViewModel.saveTunnelChanges(
tunnelConfig.copy(pingInterval = if (it.isBlank()) null else it.toLong() * 1000),
)
},
)
SubmitConfigurationTextBox(
tunnelConfig.pingCooldown?.let { (it / 1000).toString() },
stringResource(R.string.set_custom_ping_cooldown),
"(${stringResource(R.string.optional_default)} ${Constants.PING_COOLDOWN / 1000})",
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
),
isErrorValue = ::isSecondsError,
onSubmit = {
tunnelAutoTunnelViewModel.saveTunnelChanges(
tunnelConfig.copy(pingCooldown = if (it.isBlank()) null else it.toLong() * 1000),
)
},
)
},
),
)
}
add(
SelectionItem(
title = {

View File

@ -61,14 +61,6 @@ constructor(
}
}
fun onToggleRestartOnPing(tunnelConfig: TunnelConfig) = viewModelScope.launch {
appDataRepository.tunnels.save(
tunnelConfig.copy(
isPingEnabled = !tunnelConfig.isPingEnabled,
),
)
}
fun onToggleIsEthernetTunnel(tunnelConfig: TunnelConfig) = viewModelScope.launch {
if (tunnelConfig.isEthernetTunnel) {
appDataRepository.tunnels.updateEthernetTunnel(null)

View File

@ -78,15 +78,9 @@ fun <T> CoroutineScope.asChannel(flow: Flow<T>): ReceiveChannel<T> = produce {
}
}
fun Job?.onNotRunning(callback: () -> Unit) {
if (this == null || this.isCompleted || this.isCompleted) {
callback.invoke()
}
}
fun Job.cancelWithMessage(message: String) {
kotlin.runCatching {
this.cancel()
cancel()
Timber.i(message)
}
}

View File

@ -46,11 +46,10 @@ fun Peer.isReachable(): Boolean {
} else {
Constants.DEFAULT_PING_IP
}
Timber.i("Checking reachability of peer: $host")
Timber.d("Checking reachability of peer: $host")
val reachable =
InetAddress.getByName(host)
.isReachable(Constants.PING_TIMEOUT.toInt())
Timber.i("Result: reachable - $reachable")
return reachable
}