fix: auto tunnel logic and speed

closes #466
This commit is contained in:
Zane Schepke 2024-11-30 11:00:22 -05:00
parent 64a7680b81
commit 70649383e0
15 changed files with 251 additions and 329 deletions

View File

@ -157,7 +157,7 @@
android:value="true" />
</service>
<service
android:name=".service.foreground.AutoTunnelService"
android:name=".service.foreground.autotunnel.AutoTunnelService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="systemExempted"

View File

@ -1,8 +0,0 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
enum class Action {
START,
START_FOREGROUND,
STOP,
STOP_FOREGROUND,
}

View File

@ -1,107 +0,0 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
import com.zaneschepke.wireguardautotunnel.util.extensions.isMatchingToWildcardList
data class AutoTunnelState(
val vpnState: VpnState = VpnState(),
val isWifiConnected: Boolean = false,
val isEthernetConnected: Boolean = false,
val isMobileDataConnected: Boolean = false,
val currentNetworkSSID: String = "",
val settings: Settings = Settings(),
val tunnels: TunnelConfigs = emptyList(),
) {
fun isEthernetConditionMet(): Boolean {
return (
isEthernetConnected &&
settings.isTunnelOnEthernetEnabled
)
}
fun isMobileDataConditionMet(): Boolean {
return (
!isEthernetConnected &&
settings.isTunnelOnMobileDataEnabled &&
!isWifiConnected &&
isMobileDataConnected
)
}
fun isTunnelOffOnMobileDataConditionMet(): Boolean {
return (
!isEthernetConnected &&
!settings.isTunnelOnMobileDataEnabled &&
isMobileDataConnected &&
!isWifiConnected
)
}
fun isUntrustedWifiConditionMet(): Boolean {
return (
!isEthernetConnected &&
isWifiConnected &&
!isCurrentSSIDTrusted() &&
settings.isTunnelOnWifiEnabled
)
}
fun isTrustedWifiConditionMet(): Boolean {
return (
!isEthernetConnected &&
(
isWifiConnected &&
isCurrentSSIDTrusted()
)
)
}
fun isTunnelOffOnWifiConditionMet(): Boolean {
return (
!isEthernetConnected &&
(
isWifiConnected &&
!settings.isTunnelOnWifiEnabled
)
)
}
fun isTunnelOffOnNoConnectivityMet(): Boolean {
return (
!isEthernetConnected &&
!isWifiConnected &&
!isMobileDataConnected
)
}
fun isCurrentSSIDTrusted(): Boolean {
return if (settings.isWildcardsEnabled) {
settings.trustedNetworkSSIDs.isMatchingToWildcardList(currentNetworkSSID)
} else {
settings.trustedNetworkSSIDs.contains(currentNetworkSSID)
}
}
fun isCurrentSSIDActiveTunnelNetwork(): Boolean {
val currentTunnelNetworks = vpnState.tunnelConfig?.tunnelNetworks
return (
if (settings.isWildcardsEnabled) {
currentTunnelNetworks?.isMatchingToWildcardList(currentNetworkSSID)
} else {
currentTunnelNetworks?.contains(currentNetworkSSID)
}
) == true
}
fun getTunnelWithMatchingTunnelNetwork(): TunnelConfig? {
return tunnels.firstOrNull {
if (settings.isWildcardsEnabled) {
it.tunnelNetworks.isMatchingToWildcardList(currentNetworkSSID)
} else {
it.tunnelNetworks.contains(currentNetworkSSID)
}
}
}
}

View File

@ -3,6 +3,7 @@ package com.zaneschepke.wireguardautotunnel.service.foreground
import android.app.Service
import android.content.Context
import android.content.Intent
import com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.AutoTunnelService
import com.zaneschepke.wireguardautotunnel.util.SingletonHolder
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import jakarta.inject.Inject

View File

@ -0,0 +1,9 @@
package com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
sealed class AutoTunnelEvent {
data class Start(val tunnelConfig: TunnelConfig? = null) : AutoTunnelEvent()
data class Stop(val tunnelConfig: TunnelConfig?) : AutoTunnelEvent()
data object DoNothing : AutoTunnelEvent()
}

View File

@ -1,4 +1,4 @@
package com.zaneschepke.wireguardautotunnel.service.foreground
package com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel
import android.content.Intent
import android.net.NetworkCapabilities
@ -15,6 +15,7 @@ import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.AppShell
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.module.MainImmediateDispatcher
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.network.EthernetService
import com.zaneschepke.wireguardautotunnel.service.network.MobileDataService
import com.zaneschepke.wireguardautotunnel.service.network.NetworkService
@ -248,7 +249,7 @@ class AutoTunnelService : LifecycleService() {
if (results.contains(false)) {
Timber.i("Restarting VPN for ping failure")
val cooldown = vpnState.tunnelConfig.pingCooldown
tunnelService.get().bounceTunnel(vpnState.tunnelConfig)
tunnelService.get().bounceTunnel()
delay(cooldown ?: Constants.PING_COOLDOWN)
continue
}
@ -271,15 +272,15 @@ class AutoTunnelService : LifecycleService() {
old.map { it.isActive } != new.map { it.isActive }
},
) { settings, tunnels ->
Timber.d("Tunnels or settings changed!")
autoTunnelStateFlow.value.copy(
settings = settings,
tunnels = tunnels,
)
}.collect {
Timber.d("got new settings: ${it.settings}")
manageJobsBySettings(it.settings)
autoTunnelStateFlow.emit(it)
Pair(settings, tunnels)
}.collect { pair ->
manageJobsBySettings(pair.first)
autoTunnelStateFlow.update {
it.copy(
settings = pair.first,
tunnels = pair.second,
)
}
}
}
}
@ -287,12 +288,12 @@ class AutoTunnelService : LifecycleService() {
private suspend fun watchForVpnStateChanges() {
Timber.i("Starting vpn state watcher")
withContext(ioDispatcher) {
tunnelService.get().vpnState.distinctUntilChanged { old, new ->
old.tunnelConfig?.id == new.tunnelConfig?.id
}.collect { state ->
tunnelService.get().vpnState.collect { state ->
autoTunnelStateFlow.update {
it.copy(vpnState = state)
}
// TODO think about this
// What happens if we change the pinger setting while vpn is active?
state.tunnelConfig?.let {
val settings = appDataRepository.settings.getSettings()
if (it.isPingEnabled && !settings.isPingEnabled) {
@ -455,100 +456,23 @@ class AutoTunnelService : LifecycleService() {
}
}
private suspend fun getMobileDataTunnel(): TunnelConfig? {
return appDataRepository.tunnels.findByMobileDataTunnel().firstOrNull()
}
private suspend fun handleNetworkEventChanges() {
withContext(ioDispatcher) {
Timber.i("Starting network event watcher")
autoTunnelStateFlow.collect { watcherState ->
val autoTunnel = "Auto-tunnel watcher"
// delay for rapid network state changes and then collect latest
delay(Constants.WATCHER_COLLECTION_DELAY)
val activeTunnel = watcherState.vpnState.tunnelConfig
val defaultTunnel = appDataRepository.getPrimaryOrFirstTunnel()
val isTunnelDown = tunnelService.get().getState() == TunnelState.DOWN
when {
watcherState.isEthernetConditionMet() -> {
Timber.i("$autoTunnel - tunnel on on ethernet condition met")
if (isTunnelDown) {
defaultTunnel?.let {
tunnelService.get().startTunnel(it)
}
}
Timber.i("Starting auto-tunnel network event watcher")
// allow manual overrides
autoTunnelStateFlow.distinctUntilChanged { old, new ->
old.copy(vpnState = new.vpnState) == new
}.collect { watcherState ->
when (val event = watcherState.asAutoTunnelEvent()) {
is AutoTunnelEvent.Start -> {
Timber.d("Start tunnel ${event.tunnelConfig?.name}")
tunnelService.get().startTunnel(event.tunnelConfig ?: appDataRepository.getPrimaryOrFirstTunnel())
}
watcherState.isMobileDataConditionMet() -> {
Timber.i("$autoTunnel - tunnel on mobile data condition met")
val mobileDataTunnel = getMobileDataTunnel()
val tunnel =
mobileDataTunnel ?: defaultTunnel
if (isTunnelDown || activeTunnel?.isMobileDataTunnel == false) {
tunnel?.let {
tunnelService.get().startTunnel(it)
}
}
}
watcherState.isTunnelOffOnMobileDataConditionMet() -> {
Timber.i("$autoTunnel - tunnel off on mobile data met, turning vpn off")
if (!isTunnelDown) {
activeTunnel?.let {
tunnelService.get().stopTunnel(it)
}
}
}
watcherState.isUntrustedWifiConditionMet() -> {
Timber.i("Untrusted wifi condition met")
if (activeTunnel == null || watcherState.isCurrentSSIDActiveTunnelNetwork() == false ||
isTunnelDown
) {
Timber.i(
"$autoTunnel - tunnel on ssid not associated with current tunnel condition met",
)
watcherState.getTunnelWithMatchingTunnelNetwork()?.let {
Timber.i("Found tunnel associated with this SSID, bringing tunnel up: ${it.name}")
if (isTunnelDown || activeTunnel?.id != it.id) {
tunnelService.get().startTunnel(it)
}
} ?: suspend {
Timber.i("No tunnel associated with this SSID, using defaults")
val default = appDataRepository.getPrimaryOrFirstTunnel()
if (default?.name != tunnelService.get().name || isTunnelDown) {
default?.let {
tunnelService.get().startTunnel(it)
}
}
}.invoke()
}
}
watcherState.isTrustedWifiConditionMet() -> {
Timber.i(
"$autoTunnel - tunnel off on trusted wifi condition met, turning vpn off",
)
if (!isTunnelDown) activeTunnel?.let { tunnelService.get().stopTunnel(it) }
}
watcherState.isTunnelOffOnWifiConditionMet() -> {
Timber.i(
"$autoTunnel - tunnel off on wifi condition met, turning vpn off",
)
if (!isTunnelDown) activeTunnel?.let { tunnelService.get().stopTunnel(it) }
}
// TODO disable for this now
// watcherState.isTunnelOffOnNoConnectivityMet() -> {
// Timber.i(
// "$autoTunnel - tunnel off on no connectivity met, turning vpn off",
// )
// if (!isTunnelDown) activeTunnel?.let { tunnelService.get().stopTunnel(it) }
// }
else -> {
Timber.i("$autoTunnel - no condition met")
is AutoTunnelEvent.Stop -> {
Timber.d("Stop tunnel")
tunnelService.get().stopTunnel()
}
AutoTunnelEvent.DoNothing -> Timber.i("Auto-tunneling: no condition met")
}
}
}

View File

@ -0,0 +1,118 @@
package com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
import com.zaneschepke.wireguardautotunnel.util.extensions.isMatchingToWildcardList
data class AutoTunnelState(
val vpnState: VpnState = VpnState(),
val isWifiConnected: Boolean = false,
val isEthernetConnected: Boolean = false,
val isMobileDataConnected: Boolean = false,
val currentNetworkSSID: String = "",
val settings: Settings = Settings(),
val tunnels: TunnelConfigs = emptyList(),
) {
private fun isMobileDataActive(): Boolean {
return !isEthernetConnected && !isWifiConnected && isMobileDataConnected
}
private fun isMobileTunnelDataChangeNeeded(): Boolean {
val preferredTunnel = preferredMobileDataTunnel()
return preferredTunnel != null &&
vpnState.status.isUp() && preferredTunnel.id != vpnState.tunnelConfig?.id
}
private fun preferredMobileDataTunnel(): TunnelConfig? {
return tunnels.firstOrNull { it.isMobileDataTunnel } ?: tunnels.firstOrNull { it.isPrimaryTunnel }
}
private fun preferredWifiTunnel(): TunnelConfig? {
return getTunnelWithMatchingTunnelNetwork() ?: tunnels.firstOrNull { it.isPrimaryTunnel }
}
private fun isWifiActive(): Boolean {
return !isEthernetConnected && isWifiConnected
}
private fun startOnEthernet(): Boolean {
return isEthernetConnected && settings.isTunnelOnEthernetEnabled && vpnState.status.isDown()
}
private fun stopOnMobileData(): Boolean {
return isMobileDataActive() && !settings.isTunnelOnMobileDataEnabled && vpnState.status.isUp()
}
private fun startOnMobileData(): Boolean {
return isMobileDataActive() && settings.isTunnelOnMobileDataEnabled && vpnState.status.isDown()
}
private fun changeOnMobileData(): Boolean {
return isMobileDataActive() && settings.isTunnelOnMobileDataEnabled && isMobileTunnelDataChangeNeeded()
}
private fun stopOnWifi(): Boolean {
return isWifiActive() && !settings.isTunnelOnWifiEnabled && vpnState.status.isUp()
}
private fun stopOnTrustedWifi(): Boolean {
return isWifiActive() && settings.isTunnelOnWifiEnabled && vpnState.status.isUp() && isCurrentSSIDTrusted()
}
private fun startOnUntrustedWifi(): Boolean {
return isWifiActive() && settings.isTunnelOnWifiEnabled && vpnState.status.isDown() && !isCurrentSSIDTrusted()
}
private fun changeOnUntrustedWifi(): Boolean {
return isWifiActive() && settings.isTunnelOnWifiEnabled && vpnState.status.isUp() && !isCurrentSSIDTrusted() && !isWifiTunnelPreferred()
}
private fun isWifiTunnelPreferred(): Boolean {
val preferred = preferredWifiTunnel()
val vpnTunnel = vpnState.tunnelConfig
return if (preferred != null && vpnTunnel != null) {
preferred.id == vpnTunnel.id
} else {
true
}
}
// TODO add shutdown on no connectivity
fun asAutoTunnelEvent(): AutoTunnelEvent {
return when {
// ethernet scenarios
startOnEthernet() -> AutoTunnelEvent.Start()
// mobile data scenarios
stopOnMobileData() -> AutoTunnelEvent.Stop(vpnState.tunnelConfig)
startOnMobileData() -> AutoTunnelEvent.Start(tunnels.firstOrNull { it.isMobileDataTunnel })
changeOnMobileData() -> AutoTunnelEvent.Start(preferredMobileDataTunnel())
// wifi scenarios
stopOnWifi() -> AutoTunnelEvent.Stop(vpnState.tunnelConfig)
stopOnTrustedWifi() -> AutoTunnelEvent.Stop(vpnState.tunnelConfig)
startOnUntrustedWifi() -> AutoTunnelEvent.Start(preferredWifiTunnel())
changeOnUntrustedWifi() -> AutoTunnelEvent.Start(preferredWifiTunnel())
else -> AutoTunnelEvent.DoNothing
}
}
private fun isCurrentSSIDTrusted(): Boolean {
return if (settings.isWildcardsEnabled) {
settings.trustedNetworkSSIDs.isMatchingToWildcardList(currentNetworkSSID)
} else {
settings.trustedNetworkSSIDs.contains(currentNetworkSSID)
}
}
private fun getTunnelWithMatchingTunnelNetwork(): TunnelConfig? {
return tunnels.firstOrNull {
if (settings.isWildcardsEnabled) {
it.tunnelNetworks.isMatchingToWildcardList(currentNetworkSSID)
} else {
it.tunnelNetworks.contains(currentNetworkSSID)
}
}
}
}

View File

@ -4,9 +4,8 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.module.ApplicationScope
import com.zaneschepke.wireguardautotunnel.service.foreground.Action
import com.zaneschepke.wireguardautotunnel.service.foreground.AutoTunnelService
import com.zaneschepke.wireguardautotunnel.service.foreground.ServiceManager
import com.zaneschepke.wireguardautotunnel.service.foreground.autotunnel.AutoTunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
@ -47,7 +46,7 @@ class ShortcutsActivity : ComponentActivity() {
tunnelConfig?.let {
when (intent.action) {
Action.START.name -> tunnelService.get().startTunnel(it, true)
Action.STOP.name -> tunnelService.get().stopTunnel(it)
Action.STOP.name -> tunnelService.get().stopTunnel()
else -> Unit
}
}
@ -64,6 +63,11 @@ class ShortcutsActivity : ComponentActivity() {
finish()
}
enum class Action {
START,
STOP,
}
companion object {
const val LEGACY_TUNNEL_SERVICE_NAME = "WireGuardTunnelService"
const val LEGACY_AUTO_TUNNEL_SERVICE_NAME = "WireGuardConnectivityWatcherService"

View File

@ -72,7 +72,7 @@ class TunnelControlTile : TileService(), LifecycleOwner {
val lastActive = appDataRepository.getStartTunnelConfig()
lastActive?.let { tunnel ->
if (tunnel.isActive) {
tunnelService.get().stopTunnel(tunnel)
tunnelService.get().stopTunnel()
} else {
tunnelService.get().startTunnel(tunnel, true)
}

View File

@ -5,11 +5,12 @@ import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import kotlinx.coroutines.flow.StateFlow
interface TunnelService : Tunnel, org.amnezia.awg.backend.Tunnel {
suspend fun startTunnel(tunnelConfig: TunnelConfig, background: Boolean = false): Result<TunnelState>
suspend fun stopTunnel(tunnelConfig: TunnelConfig): Result<TunnelState>
suspend fun startTunnel(tunnelConfig: TunnelConfig?, background: Boolean = false)
suspend fun bounceTunnel(tunnelConfig: TunnelConfig): Result<TunnelState>
suspend fun stopTunnel()
suspend fun bounceTunnel()
val vpnState: StateFlow<VpnState>

View File

@ -24,6 +24,14 @@ enum class TunnelState {
}
}
fun isDown(): Boolean {
return this == DOWN
}
fun isUp(): Boolean {
return this == UP
}
companion object {
fun from(state: Tunnel.State): TunnelState {
return when (state) {

View File

@ -21,11 +21,13 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.amnezia.awg.backend.Tunnel
import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
import javax.inject.Provider
@ -53,7 +55,7 @@ constructor(
private var statsJob: Job? = null
private val runningHandle = AtomicBoolean(false)
private val mutex = Mutex()
private suspend fun backend(): Any {
val settings = appDataRepository.settings.getSettings()
@ -92,77 +94,58 @@ constructor(
}
}
override suspend fun startTunnel(tunnelConfig: TunnelConfig, background: Boolean): Result<TunnelState> {
return withContext(ioDispatcher) {
if (runningHandle.get() && tunnelConfig == vpnState.value.tunnelConfig) {
Timber.w("Tunnel already running")
return@withContext Result.success(vpnState.value.status)
}
runningHandle.set(true)
onBeforeStart(tunnelConfig)
val settings = appDataRepository.settings.getSettings()
if (background || settings.isKernelEnabled) startBackgroundService()
setState(tunnelConfig, TunnelState.UP).onSuccess {
updateTunnelState(it)
}.onFailure {
Timber.e(it)
onStartFailed()
private fun isTunnelAlreadyRunning(tunnelConfig: TunnelConfig): Boolean {
val isRunning = tunnelConfig == _vpnState.value.tunnelConfig && _vpnState.value.status.isUp()
if (isRunning) Timber.w("Tunnel already running")
return isRunning
}
override suspend fun startTunnel(tunnelConfig: TunnelConfig?, background: Boolean) {
if (tunnelConfig == null) return
withContext(ioDispatcher) {
mutex.withLock {
if (isTunnelAlreadyRunning(tunnelConfig)) return@withContext
onBeforeStart(background)
setState(tunnelConfig, TunnelState.UP).onSuccess {
startStatsJob()
if (it.isUp()) appDataRepository.tunnels.save(tunnelConfig.copy(isActive = true))
updateTunnelState(it, tunnelConfig)
}.onFailure {
Timber.e(it)
}
}
}
}
override suspend fun stopTunnel(tunnelConfig: TunnelConfig): Result<TunnelState> {
return withContext(ioDispatcher) {
onBeforeStop(tunnelConfig)
setState(tunnelConfig, TunnelState.DOWN).onSuccess {
updateTunnelState(it)
}.onFailure {
Timber.e(it)
onStopFailed()
}.also {
stopBackgroundService()
runningHandle.set(false)
override suspend fun stopTunnel() {
withContext(ioDispatcher) {
mutex.withLock {
if (_vpnState.value.status.isDown()) return@withContext
with(_vpnState.value) {
if (tunnelConfig == null) return@withContext
setState(tunnelConfig, TunnelState.DOWN).onSuccess {
updateTunnelState(it, null)
onStop(tunnelConfig)
stopBackgroundService()
}.onFailure {
Timber.e(it)
}
}
}
}
}
// use this when we just want to bounce tunnel and not change tunnelConfig active state
override suspend fun bounceTunnel(tunnelConfig: TunnelConfig): Result<TunnelState> {
toggleTunnel(tunnelConfig)
delay(VPN_RESTART_DELAY)
return toggleTunnel(tunnelConfig)
override suspend fun bounceTunnel() {
if (_vpnState.value.tunnelConfig == null) return
val config = _vpnState.value.tunnelConfig
stopTunnel()
startTunnel(config)
}
private suspend fun toggleTunnel(tunnelConfig: TunnelConfig): Result<TunnelState> {
return withContext(ioDispatcher) {
setState(tunnelConfig, TunnelState.TOGGLE).onSuccess {
updateTunnelState(it)
resetBackendStatistics()
}.onFailure {
Timber.e(it)
}
}
}
private suspend fun onStopFailed() {
_vpnState.value.tunnelConfig?.let {
appDataRepository.tunnels.save(it.copy(isActive = true))
}
}
private suspend fun onStartFailed() {
_vpnState.value.tunnelConfig?.let {
appDataRepository.tunnels.save(it.copy(isActive = false))
}
cancelStatsJob()
resetBackendStatistics()
runningHandle.set(false)
}
private suspend fun shutDownActiveTunnel(config: TunnelConfig) {
private suspend fun shutDownActiveTunnel() {
with(_vpnState.value) {
if (status == TunnelState.UP && tunnelConfig != config) {
tunnelConfig?.let { stopTunnel(it) }
if (status.isUp()) {
stopTunnel()
}
}
}
@ -177,51 +160,35 @@ constructor(
serviceManager.requestTunnelTileUpdate()
}
private suspend fun onBeforeStart(tunnelConfig: TunnelConfig) {
shutDownActiveTunnel(tunnelConfig)
appDataRepository.tunnels.save(tunnelConfig.copy(isActive = true))
emitVpnStateConfig(tunnelConfig)
private suspend fun onBeforeStart(background: Boolean) {
shutDownActiveTunnel()
resetBackendStatistics()
startStatsJob()
val settings = appDataRepository.settings.getSettings()
if (background || settings.isKernelEnabled) startBackgroundService()
}
private suspend fun onBeforeStop(tunnelConfig: TunnelConfig) {
private suspend fun onStop(tunnelConfig: TunnelConfig) {
appDataRepository.tunnels.save(tunnelConfig.copy(isActive = false))
cancelStatsJob()
resetBackendStatistics()
}
private fun updateTunnelState(state: TunnelState) {
_vpnState.tryEmit(
_vpnState.value.copy(
status = state,
),
)
serviceManager.requestTunnelTileUpdate()
private fun updateTunnelState(state: TunnelState, tunnelConfig: TunnelConfig?) {
_vpnState.update {
it.copy(status = state, tunnelConfig = tunnelConfig)
}
}
private fun emitBackendStatistics(statistics: TunnelStatistics) {
_vpnState.tryEmit(
_vpnState.value.copy(
statistics = statistics,
),
)
}
private fun emitVpnStateConfig(tunnelConfig: TunnelConfig) {
_vpnState.tryEmit(
_vpnState.value.copy(
tunnelConfig = tunnelConfig,
),
)
_vpnState.update {
it.copy(statistics = statistics)
}
}
private fun resetBackendStatistics() {
_vpnState.tryEmit(
_vpnState.value.copy(
statistics = null,
),
)
_vpnState.update {
it.copy(statistics = null)
}
}
override suspend fun getState(): TunnelState {
@ -265,16 +232,21 @@ constructor(
}
override fun onStateChange(newState: Tunnel.State) {
updateTunnelState(TunnelState.from(newState))
_vpnState.update {
it.copy(status = TunnelState.from(newState))
}
serviceManager.requestTunnelTileUpdate()
}
override fun onStateChange(state: State) {
updateTunnelState(TunnelState.from(state))
_vpnState.update {
it.copy(status = TunnelState.from(state))
}
serviceManager.requestTunnelTileUpdate()
}
companion object {
const val STATS_START_DELAY = 1_000L
const val VPN_STATISTIC_CHECK_INTERVAL = 1_000L
const val VPN_RESTART_DELAY = 1_000L
}
}

View File

@ -150,7 +150,7 @@ class MainActivity : AppCompatActivity() {
navController,
enterTransition = { fadeIn(tween(Constants.TRANSITION_ANIMATION_TIME)) },
exitTransition = { fadeOut(tween(Constants.TRANSITION_ANIMATION_TIME)) },
startDestination = (if (appUiState.generalState.isPinLockEnabled == true) Route.Lock else Route.Main),
startDestination = (if (appUiState.generalState.isPinLockEnabled) Route.Lock else Route.Main),
) {
composable<Route.Main> {
MainScreen(

View File

@ -155,7 +155,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) {
val intent = if (uiState.settings.isKernelEnabled) null else VpnService.prepare(context)
if (intent != null) return vpnActivity.launch(intent)
if (!checked) viewModel.onTunnelStop(tunnel).also { return }
if (!checked) viewModel.onTunnelStop().also { return }
viewModel.onTunnelStart(tunnel, uiState.settings.isKernelEnabled)
}
@ -249,8 +249,8 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
key = { tunnel -> tunnel.id },
) { tunnel ->
val isActive = uiState.tunnels.any {
it.id == tunnel.id &&
it.isActive
it.id == uiState.vpnState.tunnelConfig?.id &&
uiState.vpnState.status.isUp()
}
val expanded = uiState.generalState.isTunnelStatsExpanded
TunnelRowItem(

View File

@ -72,9 +72,9 @@ constructor(
tunnelService.get().startTunnel(tunnelConfig, background)
}
fun onTunnelStop(tunnel: TunnelConfig) = viewModelScope.launch {
fun onTunnelStop() = viewModelScope.launch {
Timber.i("Stopping active tunnel")
tunnelService.get().stopTunnel(tunnel)
tunnelService.get().stopTunnel()
}
private fun generateQrCodeDefaultName(config: String): String {