From b1dc6c5d59abfdb676af4d581a986ce809394b40 Mon Sep 17 00:00:00 2001 From: Zane Schepke Date: Thu, 2 Jan 2025 16:47:23 -0500 Subject: [PATCH] fix: make ping aware of network availability add basic tunnel error messages --- app/src/main/AndroidManifest.xml | 2 +- .../WireGuardAutoTunnel.kt | 22 +++ .../module/ServiceModule.kt | 8 +- .../module/TunnelModule.kt | 7 + .../receiver/AppUpdateReceiver.kt | 2 +- .../receiver/BootReceiver.kt | 2 +- .../service/foreground/ServiceManager.kt | 6 +- ...dService.kt => TunnelForegroundService.kt} | 6 +- .../autotunnel/model/NetworkState.kt | 6 +- .../service/shortcut/ShortcutsActivity.kt | 2 +- .../service/tile/TunnelControlTile.kt | 2 +- .../service/tunnel/TunnelService.kt | 2 +- .../service/tunnel/WireGuardTunnel.kt | 131 +++++++++++++----- .../wireguardautotunnel/ui/AppViewModel.kt | 14 +- .../wireguardautotunnel/ui/MainActivity.kt | 2 - .../ui/screens/main/MainScreen.kt | 9 +- .../ui/screens/main/MainViewModel.kt | 18 +-- app/src/main/res/values/strings.xml | 1 + .../android/en-US/changelogs/36600.txt | 6 + 19 files changed, 165 insertions(+), 83 deletions(-) rename app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/{TunnelBackgroundService.kt => TunnelForegroundService.kt} (94%) create mode 100644 fastlane/metadata/android/en-US/changelogs/36600.txt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5503c46..acf2dd3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -164,7 +164,7 @@ tools:node="merge" /> () - var backgroundService = CompletableDeferred() + var backgroundService = CompletableDeferred() var autoTunnelTile = CompletableDeferred() var tunnelControlTile = CompletableDeferred() @@ -59,10 +59,10 @@ class ServiceManager } } - suspend fun startBackgroundService(tunnelConfig: TunnelConfig?) { + suspend fun startBackgroundService(tunnelConfig: TunnelConfig) { if (backgroundService.isCompleted) return kotlin.runCatching { - startService(TunnelBackgroundService::class.java, true) + startService(TunnelForegroundService::class.java, true) backgroundService.await() backgroundService.getCompleted().start(tunnelConfig) }.onFailure { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/TunnelBackgroundService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/TunnelForegroundService.kt similarity index 94% rename from app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/TunnelBackgroundService.kt rename to app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/TunnelForegroundService.kt index 575e292..31b270e 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/TunnelBackgroundService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/TunnelForegroundService.kt @@ -16,7 +16,7 @@ import kotlinx.coroutines.CompletableDeferred import javax.inject.Inject @AndroidEntryPoint -class TunnelBackgroundService : LifecycleService() { +class TunnelForegroundService : LifecycleService() { @Inject lateinit var notificationService: NotificationService @@ -39,9 +39,9 @@ class TunnelBackgroundService : LifecycleService() { return super.onStartCommand(intent, flags, startId) } - fun start(tunnelConfig: TunnelConfig?) { + fun start(tunnelConfig: TunnelConfig) { ServiceCompat.startForeground( - this, + this@TunnelForegroundService, NotificationService.KERNEL_SERVICE_NOTIFICATION_ID, createNotification(tunnelConfig), Constants.SYSTEM_EXEMPT_SERVICE_TYPE_ID, diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/autotunnel/model/NetworkState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/autotunnel/model/NetworkState.kt index 35bf8d2..25bb21a 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/autotunnel/model/NetworkState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/foreground/autotunnel/model/NetworkState.kt @@ -5,4 +5,8 @@ data class NetworkState( val isMobileDataConnected: Boolean = false, val isEthernetConnected: Boolean = false, val wifiName: String? = null, -) +) { + fun hasNoCapabilities(): Boolean { + return !isWifiConnected && !isMobileDataConnected && !isEthernetConnected + } +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsActivity.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsActivity.kt index a520bb2..616f699 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsActivity.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/shortcut/ShortcutsActivity.kt @@ -45,7 +45,7 @@ class ShortcutsActivity : ComponentActivity() { Timber.d("Shortcut action on name: ${tunnelConfig?.name}") tunnelConfig?.let { when (intent.action) { - Action.START.name -> tunnelService.get().startTunnel(it, true) + Action.START.name -> tunnelService.get().startTunnel(it) Action.STOP.name -> tunnelService.get().stopTunnel() else -> Unit } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt index d5a1c5a..03c237d 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tile/TunnelControlTile.kt @@ -66,7 +66,7 @@ class TunnelControlTile : TileService() { applicationScope.launch { if (tunnelService.vpnState.value.status.isUp()) return@launch tunnelService.stopTunnel() appDataRepository.getStartTunnelConfig()?.let { - tunnelService.startTunnel(it, true) + tunnelService.startTunnel(it) } } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/TunnelService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/TunnelService.kt index 7761073..bb6a709 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/TunnelService.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/TunnelService.kt @@ -6,7 +6,7 @@ import kotlinx.coroutines.flow.StateFlow interface TunnelService : Tunnel, org.amnezia.awg.backend.Tunnel { - suspend fun startTunnel(tunnelConfig: TunnelConfig?, background: Boolean = false) + suspend fun startTunnel(tunnelConfig: TunnelConfig?) suspend fun stopTunnel() 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 2243f0c..0773fd7 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 @@ -2,33 +2,44 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel import com.wireguard.android.backend.Backend import com.wireguard.android.backend.Tunnel.State -import com.zaneschepke.wireguardautotunnel.R +import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository import com.zaneschepke.wireguardautotunnel.module.ApplicationScope +import com.zaneschepke.wireguardautotunnel.module.Ethernet import com.zaneschepke.wireguardautotunnel.module.IoDispatcher import com.zaneschepke.wireguardautotunnel.module.Kernel +import com.zaneschepke.wireguardautotunnel.module.MobileData +import com.zaneschepke.wireguardautotunnel.module.Wifi import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager -import com.zaneschepke.wireguardautotunnel.service.notification.NotificationAction +import com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.model.NetworkState +import com.zaneschepke.wireguardautotunnel.service.network.NetworkService import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService import com.zaneschepke.wireguardautotunnel.service.notification.NotificationService.Companion.VPN_NOTIFICATION_ID -import com.zaneschepke.wireguardautotunnel.service.notification.WireGuardNotification import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.AmneziaStatistics import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.WireGuardStatistics +import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController import com.zaneschepke.wireguardautotunnel.util.Constants +import com.zaneschepke.wireguardautotunnel.R +import com.zaneschepke.wireguardautotunnel.service.notification.WireGuardNotification +import com.zaneschepke.wireguardautotunnel.util.StringValue import com.zaneschepke.wireguardautotunnel.util.extensions.asAmBackendState import com.zaneschepke.wireguardautotunnel.util.extensions.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.FlowPreview import kotlinx.coroutines.Job import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex @@ -37,6 +48,7 @@ import kotlinx.coroutines.withContext import org.amnezia.awg.backend.Tunnel import timber.log.Timber import java.net.InetAddress +import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject import javax.inject.Provider @@ -51,6 +63,9 @@ constructor( @IoDispatcher private val ioDispatcher: CoroutineDispatcher, private val serviceManager: ServiceManager, private val notificationService: NotificationService, + @Wifi private val wifiService: NetworkService, + @MobileData private val mobileDataService: NetworkService, + @Ethernet private val ethernetService: NetworkService, ) : TunnelService { private val _vpnState = MutableStateFlow(VpnState()) @@ -59,9 +74,11 @@ constructor( private var statsJob: Job? = null private var tunnelChangesJob: Job? = null private var pingJob: Job? = null + private var networkJob: Job? = null @get:Synchronized @set:Synchronized private var isKernelBackend: Boolean? = null + private val isNetworkAvailable = AtomicBoolean(false) private val tunnelControlMutex = Mutex() @@ -88,6 +105,23 @@ constructor( } } + // TODO refactor duplicate + @OptIn(FlowPreview::class) + private fun combineNetworkEventsJob(): Flow { + return combine( + wifiService.status, + mobileDataService.status, + ethernetService.status, + ) { wifi, mobileData, ethernet -> + NetworkState( + wifi.available, + mobileData.available, + ethernet.available, + wifi.name, + ) + }.distinctUntilChanged() + } + private suspend fun setState(tunnelConfig: TunnelConfig, tunnelState: TunnelState): Result { return runCatching { when (val backend = backend()) { @@ -113,20 +147,43 @@ constructor( } } - override suspend fun startTunnel(tunnelConfig: TunnelConfig?, background: Boolean) { + override suspend fun startTunnel(tunnelConfig: TunnelConfig?) { withContext(ioDispatcher) { if (tunnelConfig == null || isTunnelAlreadyRunning(tunnelConfig)) return@withContext - onBeforeStart(background) + onBeforeStart(tunnelConfig) updateTunnelConfig(tunnelConfig) // need to update this here + appDataRepository.tunnels.save(tunnelConfig.copy(isActive = true)) withServiceActive { setState(tunnelConfig, TunnelState.UP).onSuccess { updateTunnelState(it, tunnelConfig) - onTunnelStart(tunnelConfig, background) + startActiveTunnelJobs() + }.onFailure { + onTunnelStop(tunnelConfig) + // TODO improve this with better statuses and handling + showTunnelStartFailed() } } } } + private fun showTunnelStartFailed() { + if (WireGuardAutoTunnel.isForeground()) { + SnackbarController.showMessage(StringValue.StringResource(R.string.error_tunnel_start)) + } else { + launchStartFailedNotification() + } + } + + private fun launchStartFailedNotification() { + with(notificationService) { + val notification = createNotification( + WireGuardNotification.NotificationChannels.VPN, + title = context.getString(R.string.error_tunnel_start), + ) + show(VPN_NOTIFICATION_ID, notification) + } + } + override suspend fun stopTunnel() { withContext(ioDispatcher) { if (_vpnState.value.status.isDown()) return@withContext @@ -200,31 +257,10 @@ constructor( } } - private suspend fun onBeforeStart(background: Boolean) { + private suspend fun onBeforeStart(tunnelConfig: TunnelConfig) { with(_vpnState.value) { if (status.isUp()) stopTunnel() else clearJobsAndStats() - if (isKernelBackend == true || background) serviceManager.startBackgroundService(tunnelConfig) - } - } - - private suspend fun onTunnelStart(tunnelConfig: TunnelConfig, background: Boolean) { - startActiveTunnelJobs() - if (_vpnState.value.status.isUp()) { - appDataRepository.tunnels.save(tunnelConfig.copy(isActive = true)) - } - if (isKernelBackend == false && !background) launchUserspaceTunnelNotification() - } - - private fun launchUserspaceTunnelNotification() { - with(notificationService) { - val notification = createNotification( - WireGuardNotification.NotificationChannels.VPN, - title = "${context.getString(R.string.tunnel_running)} - ${_vpnState.value.tunnelConfig?.name}", - actions = listOf( - notificationService.createNotificationAction(NotificationAction.TUNNEL_OFF), - ), - ) - show(VPN_NOTIFICATION_ID, notification) + serviceManager.startBackgroundService(tunnelConfig) } } @@ -278,14 +314,21 @@ constructor( statsJob?.cancelWithMessage("Tunnel stats job cancelled") tunnelChangesJob?.cancelWithMessage("Tunnel changes job cancelled") pingJob?.cancelWithMessage("Ping job cancelled") + networkJob?.cancelWithMessage("Network job cancelled") } override fun startActiveTunnelJobs() { statsJob = startTunnelStatisticsJob() tunnelChangesJob = startTunnelConfigChangesJob() - if (_vpnState.value.tunnelConfig?.isPingEnabled == true) pingJob = startPingJob() + if (_vpnState.value.tunnelConfig?.isPingEnabled == true) { + startPingJobs() + } } + private fun startPingJobs() { + pingJob = startPingJob() + networkJob = startNetworkJob() + } override fun getName(): String { return _vpnState.value.tunnelConfig?.name ?: "" } @@ -331,6 +374,7 @@ constructor( if (this == null) return if (!isPingEnabled && pingJob?.isActive == true) { pingJob?.cancelWithMessage("Ping job cancelled") + networkJob?.cancelWithMessage("Network job cancelled") return } restartPingJob() @@ -339,7 +383,8 @@ constructor( private fun restartPingJob() { pingJob?.cancelWithMessage("Ping job cancelled") - pingJob = startPingJob() + networkJob?.cancelWithMessage("Network job cancelled") + startPingJobs() } private fun startTunnelConfigChangesJob() = applicationScope.launch(ioDispatcher) { @@ -376,13 +421,16 @@ constructor( do { run { with(_vpnState.value) { - // TODO ignore when no connectivity - if (status.isUp() && tunnelConfig != null) { + if (status.isUp() && tunnelConfig != null && isNetworkAvailable.get()) { 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) + if (isNetworkAvailable.get()) { + Timber.i("Ping result: target was not reachable, bouncing the tunnel") + bounceTunnel() + delay(tunnelConfig.pingCooldown ?: Constants.PING_COOLDOWN) + } else { + Timber.i("Ping result: target was not reachable, but not network available") + } return@run } else { Timber.i("Ping result: all ping targets were reached successfully") @@ -394,6 +442,17 @@ constructor( } while (true) } + private fun startNetworkJob() = applicationScope.launch(ioDispatcher) { + combineNetworkEventsJob().collect { + Timber.d("New network state: $it") + if (!it.isWifiConnected && !it.isEthernetConnected && !it.isMobileDataConnected) { + isNetworkAvailable.set(false) + } else { + isNetworkAvailable.set(true) + } + } + } + override fun onStateChange(newState: Tunnel.State) { _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 9398f4c..32dcf65 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/AppViewModel.kt @@ -112,12 +112,14 @@ constructor( } private suspend fun initTunnel() { - if (tunnelService.get().getState() == TunnelState.UP) tunnelService.get().startActiveTunnelJobs() - val activeTunnels = appDataRepository.tunnels.getActive() - if (activeTunnels.isNotEmpty() && - tunnelService.get().getState() == TunnelState.DOWN - ) { - tunnelService.get().startTunnel(activeTunnels.first()) + withContext(ioDispatcher) { + if (tunnelService.get().getState() == TunnelState.UP) tunnelService.get().startActiveTunnelJobs() + val activeTunnels = appDataRepository.tunnels.getActive() + if (activeTunnels.isNotEmpty() && + tunnelService.get().getState() == TunnelState.DOWN + ) { + tunnelService.get().startTunnel(activeTunnels.first()) + } } } 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 897c9c2..3816648 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/MainActivity.kt @@ -13,7 +13,6 @@ import androidx.appcompat.app.AppCompatActivity import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.WindowInsets @@ -142,7 +141,6 @@ class MainActivity : AppCompatActivity() { SnackbarControllerProvider { host -> WireguardAutoTunnelTheme(theme = appUiState.generalState.theme) { Scaffold( - modifier = Modifier.background(color = MaterialTheme.colorScheme.background), contentWindowInsets = WindowInsets(0), snackbarHost = { SnackbarHost(host) { snackbarData: SnackbarData -> diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt index 441da42..e1abcf1 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt @@ -82,7 +82,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState) val startAutoTunnel = withVpnPermission { viewModel.onToggleAutoTunnel() } val startTunnel = withVpnPermission { - viewModel.onTunnelStart(it, uiState.settings.isKernelEnabled) + viewModel.onTunnelStart(it) } val autoTunnelToggleBattery = withIgnoreBatteryOpt(uiState.generalState.isBatteryOptimizationDisableShown) { if (!uiState.generalState.isBatteryOptimizationDisableShown) viewModel.setBatteryOptimizeDisableShown() @@ -129,7 +129,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState) fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) { if (!checked) viewModel.onTunnelStop().also { return } if (uiState.settings.isKernelEnabled) { - viewModel.onTunnelStart(tunnel, uiState.settings.isKernelEnabled) + viewModel.onTunnelStart(tunnel) } else { startTunnel.invoke(tunnel) } @@ -226,8 +226,9 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState) ) { tunnel -> val expanded = uiState.generalState.isTunnelStatsExpanded TunnelRowItem( - tunnel.id == uiState.vpnState.tunnelConfig?.id && - uiState.vpnState.status.isUp(), + tunnel.id == uiState.vpnState.tunnelConfig?.id && ( + uiState.vpnState.status.isUp() || (uiState.settings.isKernelEnabled && tunnel.isActive) + ), expanded, selectedTunnel?.id == tunnel.id, tunnel, diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt index c96912d..603ceaf 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainViewModel.kt @@ -67,9 +67,9 @@ constructor( appDataRepository.appState.setTunnelStatsExpanded(expanded) } - fun onTunnelStart(tunnelConfig: TunnelConfig, background: Boolean) = viewModelScope.launch { + fun onTunnelStart(tunnelConfig: TunnelConfig) = viewModelScope.launch { Timber.i("Starting tunnel ${tunnelConfig.name}") - tunnelService.get().startTunnel(tunnelConfig, background) + tunnelService.get().startTunnel(tunnelConfig) } fun onTunnelStop() = viewModelScope.launch { @@ -86,20 +86,6 @@ constructor( } } - private fun generateQrCodeTunnelName(config: String): String { - var defaultName = generateQrCodeDefaultName(config) - val lines = config.lines().toMutableList() - val linesIterator = lines.iterator() - while (linesIterator.hasNext()) { - val next = linesIterator.next() - if (next.contains(Constants.QR_CODE_NAME_PROPERTY)) { - defaultName = next.substringAfter(Constants.QR_CODE_NAME_PROPERTY).trim() - break - } - } - return defaultName - } - private suspend fun makeTunnelNameUnique(name: String): String { return withContext(ioDispatcher) { val tunnels = appDataRepository.tunnels.getAll() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 926961f..e1eb9c5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -202,4 +202,5 @@ Remove Amnezia compatibility Exclude LAN Include LAN + Failed to starting tunnel diff --git a/fastlane/metadata/android/en-US/changelogs/36600.txt b/fastlane/metadata/android/en-US/changelogs/36600.txt new file mode 100644 index 0000000..934bb29 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/36600.txt @@ -0,0 +1,6 @@ +What's new: +- Ping feature now works independent of auto tunnel +- Added convenience action for Amnezia compatibility +- Added convenience action for excluding LAN from tunnel +- Added debounce delay tuning option for auto tunnel +- Many bug fixes and improvements