diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/TunnelConfig.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/TunnelConfig.kt index f6008ca..c799f51 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/TunnelConfig.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/data/domain/TunnelConfig.kt @@ -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 { 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 f424722..0a015bb 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.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 { 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> { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt index c783e4f..2243f0c 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt @@ -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 { 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 { + 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)) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt index 11f0f6c..9398f4c 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt @@ -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, diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt index bc3c7ab..897c9c2 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt @@ -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 { val args = it.toRoute() val config = appUiState.tunnels.first { it.id == args.id } - OptionsScreen(config, viewModel) + OptionsScreen(config) } composable { 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() - } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/TopNavBar.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/TopNavBar.kt index 494ee5f..05ad153 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/TopNavBar.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/navigation/TopNavBar.kt @@ -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, 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 ccf5832..695b487 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 @@ -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 = { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/autotunnel/AutoTunnelViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/autotunnel/AutoTunnelViewModel.kt index 2ea8642..9670d2d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/autotunnel/AutoTunnelViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/autotunnel/AutoTunnelViewModel.kt @@ -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( diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunneloptions/TunnelOptionsScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunneloptions/TunnelOptionsScreen.kt index 8b4aafa..971153d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunneloptions/TunnelOptionsScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunneloptions/TunnelOptionsScreen.kt @@ -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), + ) + }, + ) + }, + ), + ) + } + }, + ) } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunneloptions/TunnelOptionsViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunneloptions/TunnelOptionsViewModel.kt new file mode 100644 index 0000000..0019fe2 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunneloptions/TunnelOptionsViewModel.kt @@ -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) + } +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunneloptions/tunnelautotunnel/TunnelAutoTunnelScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunneloptions/tunnelautotunnel/TunnelAutoTunnelScreen.kt index 02c7c8c..d8b1e83 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunneloptions/tunnelautotunnel/TunnelAutoTunnelScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunneloptions/tunnelautotunnel/TunnelAutoTunnelScreen.kt @@ -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 = { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunneloptions/tunnelautotunnel/TunnelAutoTunnelViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunneloptions/tunnelautotunnel/TunnelAutoTunnelViewModel.kt index 0a9de2f..19095ba 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunneloptions/tunnelautotunnel/TunnelAutoTunnelViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/tunneloptions/tunnelautotunnel/TunnelAutoTunnelViewModel.kt @@ -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) diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/CoroutineExtensions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/CoroutineExtensions.kt index ecbc3bd..b7e37bf 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/CoroutineExtensions.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/CoroutineExtensions.kt @@ -78,15 +78,9 @@ fun CoroutineScope.asChannel(flow: Flow): ReceiveChannel = 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) } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/TunnelExtensions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/TunnelExtensions.kt index e49ef3a..84407e0 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/TunnelExtensions.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/TunnelExtensions.kt @@ -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 }