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:
parent
ba064b267f
commit
f772dc0f8a
|
@ -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 {
|
||||
|
|
|
@ -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>> {
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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 = {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue