fix: auto tunnel overrides

This commit is contained in:
Zane Schepke 2024-09-22 00:24:38 -04:00
parent a362327fa3
commit d330fa4c28
13 changed files with 172 additions and 158 deletions

View File

@ -4,7 +4,6 @@ data class GeneralState(
val isLocationDisclosureShown: Boolean = LOCATION_DISCLOSURE_SHOWN_DEFAULT, val isLocationDisclosureShown: Boolean = LOCATION_DISCLOSURE_SHOWN_DEFAULT,
val isBatteryOptimizationDisableShown: Boolean = BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT, val isBatteryOptimizationDisableShown: Boolean = BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT,
val isPinLockEnabled: Boolean = PIN_LOCK_ENABLED_DEFAULT, val isPinLockEnabled: Boolean = PIN_LOCK_ENABLED_DEFAULT,
val lastActiveTunnelId: Int? = null,
) { ) {
companion object { companion object {
const val LOCATION_DISCLOSURE_SHOWN_DEFAULT = false const val LOCATION_DISCLOSURE_SHOWN_DEFAULT = false

View File

@ -2,57 +2,47 @@ package com.zaneschepke.wireguardautotunnel.data.repository
import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager import com.zaneschepke.wireguardautotunnel.data.datastore.DataStoreManager
import com.zaneschepke.wireguardautotunnel.data.domain.GeneralState import com.zaneschepke.wireguardautotunnel.data.domain.GeneralState
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
class DataStoreAppStateRepository( class DataStoreAppStateRepository(
private val dataStoreManager: DataStoreManager, private val dataStoreManager: DataStoreManager,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : ) :
AppStateRepository { AppStateRepository {
override suspend fun isLocationDisclosureShown(): Boolean { override suspend fun isLocationDisclosureShown(): Boolean {
return withContext(ioDispatcher) { return dataStoreManager.getFromStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN)
dataStoreManager.getFromStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN) ?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT
?: GeneralState.LOCATION_DISCLOSURE_SHOWN_DEFAULT
}
} }
override suspend fun setLocationDisclosureShown(shown: Boolean) { override suspend fun setLocationDisclosureShown(shown: Boolean) {
withContext(ioDispatcher) { dataStoreManager.saveToDataStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN, shown) } dataStoreManager.saveToDataStore(DataStoreManager.LOCATION_DISCLOSURE_SHOWN, shown)
} }
override suspend fun isPinLockEnabled(): Boolean { override suspend fun isPinLockEnabled(): Boolean {
return withContext(ioDispatcher) { return dataStoreManager.getFromStore(DataStoreManager.IS_PIN_LOCK_ENABLED)
dataStoreManager.getFromStore(DataStoreManager.IS_PIN_LOCK_ENABLED) ?: GeneralState.PIN_LOCK_ENABLED_DEFAULT
?: GeneralState.PIN_LOCK_ENABLED_DEFAULT
}
} }
override suspend fun setPinLockEnabled(enabled: Boolean) { override suspend fun setPinLockEnabled(enabled: Boolean) {
withContext(ioDispatcher) { dataStoreManager.saveToDataStore(DataStoreManager.IS_PIN_LOCK_ENABLED, enabled) } dataStoreManager.saveToDataStore(DataStoreManager.IS_PIN_LOCK_ENABLED, enabled)
} }
override suspend fun isBatteryOptimizationDisableShown(): Boolean { override suspend fun isBatteryOptimizationDisableShown(): Boolean {
return withContext(ioDispatcher) { return dataStoreManager.getFromStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN)
dataStoreManager.getFromStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN) ?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT
?: GeneralState.BATTERY_OPTIMIZATION_DISABLE_SHOWN_DEFAULT
}
} }
override suspend fun setBatteryOptimizationDisableShown(shown: Boolean) { override suspend fun setBatteryOptimizationDisableShown(shown: Boolean) {
withContext(ioDispatcher) { dataStoreManager.saveToDataStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN, shown) } dataStoreManager.saveToDataStore(DataStoreManager.BATTERY_OPTIMIZE_DISABLE_SHOWN, shown)
} }
override suspend fun getCurrentSsid(): String? { override suspend fun getCurrentSsid(): String? {
return withContext(ioDispatcher) { dataStoreManager.getFromStore(DataStoreManager.CURRENT_SSID) } return dataStoreManager.getFromStore(DataStoreManager.CURRENT_SSID)
} }
override suspend fun setCurrentSsid(ssid: String) { override suspend fun setCurrentSsid(ssid: String) {
withContext(ioDispatcher) { dataStoreManager.saveToDataStore(DataStoreManager.CURRENT_SSID, ssid) } dataStoreManager.saveToDataStore(DataStoreManager.CURRENT_SSID, ssid)
} }
override val generalStateFlow: Flow<GeneralState> = override val generalStateFlow: Flow<GeneralState> =

View File

@ -72,8 +72,8 @@ class RepositoryModule {
@Provides @Provides
@Singleton @Singleton
fun provideGeneralStateRepository(dataStoreManager: DataStoreManager, @IoDispatcher ioDispatcher: CoroutineDispatcher): AppStateRepository { fun provideGeneralStateRepository(dataStoreManager: DataStoreManager): AppStateRepository {
return DataStoreAppStateRepository(dataStoreManager, ioDispatcher) return DataStoreAppStateRepository(dataStoreManager)
} }
@Provides @Provides

View File

@ -8,6 +8,7 @@ import com.wireguard.android.backend.WgQuickBackend
import com.wireguard.android.util.RootShell import com.wireguard.android.util.RootShell
import com.wireguard.android.util.ToolsInstaller import com.wireguard.android.util.ToolsInstaller
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.data.repository.TunnelConfigRepository
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelService
import com.zaneschepke.wireguardautotunnel.service.tunnel.WireGuardTunnel import com.zaneschepke.wireguardautotunnel.service.tunnel.WireGuardTunnel
import dagger.Module import dagger.Module
@ -61,11 +62,13 @@ class TunnelModule {
amneziaBackend: Provider<org.amnezia.awg.backend.Backend>, amneziaBackend: Provider<org.amnezia.awg.backend.Backend>,
@Kernel kernelBackend: Provider<Backend>, @Kernel kernelBackend: Provider<Backend>,
appDataRepository: AppDataRepository, appDataRepository: AppDataRepository,
tunnelConfigRepository: TunnelConfigRepository,
@ApplicationScope applicationScope: CoroutineScope, @ApplicationScope applicationScope: CoroutineScope,
@IoDispatcher ioDispatcher: CoroutineDispatcher, @IoDispatcher ioDispatcher: CoroutineDispatcher,
): TunnelService { ): TunnelService {
return WireGuardTunnel( return WireGuardTunnel(
amneziaBackend, amneziaBackend,
tunnelConfigRepository,
kernelBackend, kernelBackend,
appDataRepository, appDataRepository,
applicationScope, applicationScope,

View File

@ -33,6 +33,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -71,7 +72,7 @@ class AutoTunnelService : LifecycleService() {
@MainImmediateDispatcher @MainImmediateDispatcher
lateinit var mainImmediateDispatcher: CoroutineDispatcher lateinit var mainImmediateDispatcher: CoroutineDispatcher
private val networkEventsFlow = MutableStateFlow(AutoTunnelState()) private val autoTunnelStateFlow = MutableStateFlow(AutoTunnelState())
private var wakeLock: PowerManager.WakeLock? = null private var wakeLock: PowerManager.WakeLock? = null
@ -132,6 +133,7 @@ class AutoTunnelService : LifecycleService() {
initWakeLock() initWakeLock()
} }
startSettingsJob() startSettingsJob()
startVpnStateJob()
}.onFailure { }.onFailure {
Timber.e(it) Timber.e(it)
} }
@ -191,6 +193,10 @@ class AutoTunnelService : LifecycleService() {
watchForSettingsChanges() watchForSettingsChanges()
} }
private fun startVpnStateJob() = lifecycleScope.launch {
watchForVpnStateChanges()
}
private fun startWifiJob() = lifecycleScope.launch { private fun startWifiJob() = lifecycleScope.launch {
watchForWifiConnectivityChanges() watchForWifiConnectivityChanges()
} }
@ -218,7 +224,7 @@ class AutoTunnelService : LifecycleService() {
when (status) { when (status) {
is NetworkStatus.Available -> { is NetworkStatus.Available -> {
Timber.i("Gained Mobile data connection") Timber.i("Gained Mobile data connection")
networkEventsFlow.update { autoTunnelStateFlow.update {
it.copy( it.copy(
isMobileDataConnected = true, isMobileDataConnected = true,
) )
@ -226,7 +232,7 @@ class AutoTunnelService : LifecycleService() {
} }
is NetworkStatus.CapabilitiesChanged -> { is NetworkStatus.CapabilitiesChanged -> {
networkEventsFlow.update { autoTunnelStateFlow.update {
it.copy( it.copy(
isMobileDataConnected = true, isMobileDataConnected = true,
) )
@ -235,7 +241,7 @@ class AutoTunnelService : LifecycleService() {
} }
is NetworkStatus.Unavailable -> { is NetworkStatus.Unavailable -> {
networkEventsFlow.update { autoTunnelStateFlow.update {
it.copy( it.copy(
isMobileDataConnected = false, isMobileDataConnected = false,
) )
@ -284,16 +290,8 @@ class AutoTunnelService : LifecycleService() {
} }
} }
private fun updateSettings(settings: Settings) {
networkEventsFlow.update {
it.copy(
settings = settings,
)
}
}
private fun onAutoTunnelPause(paused: Boolean) { private fun onAutoTunnelPause(paused: Boolean) {
if (networkEventsFlow.value.settings.isAutoTunnelPaused if (autoTunnelStateFlow.value.settings.isAutoTunnelPaused
!= paused != paused
) { ) {
when (paused) { when (paused) {
@ -307,19 +305,36 @@ class AutoTunnelService : LifecycleService() {
Timber.i("Starting settings watcher") Timber.i("Starting settings watcher")
withContext(ioDispatcher) { withContext(ioDispatcher) {
appDataRepository.settings.getSettingsFlow().combine( appDataRepository.settings.getSettingsFlow().combine(
appDataRepository.tunnels.getTunnelConfigsFlow(), // ignore isActive changes to allow manual tunnel overrides
appDataRepository.tunnels.getTunnelConfigsFlow().distinctUntilChanged { old, new ->
old.map { it.isActive } != new.map { it.isActive }
},
) { settings, tunnels -> ) { settings, tunnels ->
val activeTunnel = tunnels.firstOrNull { it.isActive } autoTunnelStateFlow.value.copy(
if (!settings.isPingEnabled) { settings = settings,
settings.copy(isPingEnabled = activeTunnel?.isPingEnabled ?: false) tunnels = tunnels,
} else { )
settings
}
}.collect { }.collect {
Timber.d("Settings change: $it") onAutoTunnelPause(it.settings.isAutoTunnelPaused)
onAutoTunnelPause(it.isAutoTunnelPaused) manageJobsBySettings(it.settings)
updateSettings(it) autoTunnelStateFlow.emit(it)
manageJobsBySettings(it) }
}
}
private suspend fun watchForVpnStateChanges() {
Timber.i("Starting vpn state watcher")
withContext(ioDispatcher) {
tunnelService.get().vpnState.collect { state ->
state.tunnelConfig?.let {
val settings = appDataRepository.settings.getSettings()
if (it.isPingEnabled && !settings.isPingEnabled) {
pingJob.onNotRunning { pingJob = startPingJob() }
}
if (!it.isPingEnabled && !settings.isPingEnabled) {
cancelAndResetPingJob()
}
}
} }
} }
} }
@ -375,7 +390,7 @@ class AutoTunnelService : LifecycleService() {
} }
private fun updateEthernet(connected: Boolean) { private fun updateEthernet(connected: Boolean) {
networkEventsFlow.update { autoTunnelStateFlow.update {
it.copy( it.copy(
isEthernetConnected = connected, isEthernetConnected = connected,
) )
@ -413,7 +428,7 @@ class AutoTunnelService : LifecycleService() {
when (status) { when (status) {
is NetworkStatus.Available -> { is NetworkStatus.Available -> {
Timber.i("Gained Wi-Fi connection") Timber.i("Gained Wi-Fi connection")
networkEventsFlow.update { autoTunnelStateFlow.update {
it.copy( it.copy(
isWifiConnected = true, isWifiConnected = true,
) )
@ -422,7 +437,7 @@ class AutoTunnelService : LifecycleService() {
is NetworkStatus.CapabilitiesChanged -> { is NetworkStatus.CapabilitiesChanged -> {
Timber.i("Wifi capabilities changed") Timber.i("Wifi capabilities changed")
networkEventsFlow.update { autoTunnelStateFlow.update {
it.copy( it.copy(
isWifiConnected = true, isWifiConnected = true,
) )
@ -435,7 +450,7 @@ class AutoTunnelService : LifecycleService() {
Timber.i("Detected valid SSID") Timber.i("Detected valid SSID")
} }
appDataRepository.appState.setCurrentSsid(name) appDataRepository.appState.setCurrentSsid(name)
networkEventsFlow.update { autoTunnelStateFlow.update {
it.copy( it.copy(
currentNetworkSSID = name, currentNetworkSSID = name,
) )
@ -444,7 +459,7 @@ class AutoTunnelService : LifecycleService() {
} }
is NetworkStatus.Unavailable -> { is NetworkStatus.Unavailable -> {
networkEventsFlow.update { autoTunnelStateFlow.update {
it.copy( it.copy(
isWifiConnected = false, isWifiConnected = false,
) )
@ -460,10 +475,6 @@ class AutoTunnelService : LifecycleService() {
return appDataRepository.tunnels.findByMobileDataTunnel().firstOrNull() return appDataRepository.tunnels.findByMobileDataTunnel().firstOrNull()
} }
private suspend fun getSsidTunnel(ssid: String): TunnelConfig? {
return appDataRepository.tunnels.findByTunnelNetworksName(ssid).firstOrNull()
}
private fun isTunnelDown(): Boolean { private fun isTunnelDown(): Boolean {
return tunnelService.get().vpnState.value.status == TunnelState.DOWN return tunnelService.get().vpnState.value.status == TunnelState.DOWN
} }
@ -471,7 +482,7 @@ class AutoTunnelService : LifecycleService() {
private suspend fun handleNetworkEventChanges() { private suspend fun handleNetworkEventChanges() {
withContext(ioDispatcher) { withContext(ioDispatcher) {
Timber.i("Starting network event watcher") Timber.i("Starting network event watcher")
networkEventsFlow.collectLatest { watcherState -> autoTunnelStateFlow.collectLatest { watcherState ->
val autoTunnel = "Auto-tunnel watcher" val autoTunnel = "Auto-tunnel watcher"
if (!watcherState.settings.isAutoTunnelPaused) { if (!watcherState.settings.isAutoTunnelPaused) {
// delay for rapid network state changes and then collect latest // delay for rapid network state changes and then collect latest
@ -517,7 +528,7 @@ class AutoTunnelService : LifecycleService() {
Timber.i( Timber.i(
"$autoTunnel - tunnel on ssid not associated with current tunnel condition met", "$autoTunnel - tunnel on ssid not associated with current tunnel condition met",
) )
getSsidTunnel(watcherState.currentNetworkSSID)?.let { watcherState.tunnels.firstOrNull { it.tunnelNetworks.isMatchingToWildcardList(watcherState.currentNetworkSSID) }?.let {
Timber.i("Found tunnel associated with this SSID, bringing tunnel up: ${it.name}") Timber.i("Found tunnel associated with this SSID, bringing tunnel up: ${it.name}")
if (isTunnelDown() || activeTunnel?.id != it.id) { if (isTunnelDown() || activeTunnel?.id != it.id) {
tunnelService.get().startTunnel(it) tunnelService.get().startTunnel(it)

View File

@ -1,6 +1,7 @@
package com.zaneschepke.wireguardautotunnel.service.foreground package com.zaneschepke.wireguardautotunnel.service.foreground
import com.zaneschepke.wireguardautotunnel.data.domain.Settings import com.zaneschepke.wireguardautotunnel.data.domain.Settings
import com.zaneschepke.wireguardautotunnel.util.extensions.TunnelConfigs
import com.zaneschepke.wireguardautotunnel.util.extensions.isMatchingToWildcardList import com.zaneschepke.wireguardautotunnel.util.extensions.isMatchingToWildcardList
data class AutoTunnelState( data class AutoTunnelState(
@ -9,6 +10,7 @@ data class AutoTunnelState(
val isMobileDataConnected: Boolean = false, val isMobileDataConnected: Boolean = false,
val currentNetworkSSID: String = "", val currentNetworkSSID: String = "",
val settings: Settings = Settings(), val settings: Settings = Settings(),
val tunnels: TunnelConfigs = emptyList(),
) { ) {
fun isEthernetConditionMet(): Boolean { fun isEthernetConditionMet(): Boolean {
return ( return (

View File

@ -2,24 +2,24 @@ package com.zaneschepke.wireguardautotunnel.service.tunnel
import com.wireguard.android.backend.Backend import com.wireguard.android.backend.Backend
import com.wireguard.android.backend.Tunnel.State import com.wireguard.android.backend.Tunnel.State
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository 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.ApplicationScope
import com.zaneschepke.wireguardautotunnel.module.IoDispatcher import com.zaneschepke.wireguardautotunnel.module.IoDispatcher
import com.zaneschepke.wireguardautotunnel.module.Kernel import com.zaneschepke.wireguardautotunnel.module.Kernel
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.AmneziaStatistics import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.AmneziaStatistics
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.WireGuardStatistics import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.WireGuardStatistics
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.amnezia.awg.backend.Tunnel import org.amnezia.awg.backend.Tunnel
@ -31,6 +31,7 @@ class WireGuardTunnel
@Inject @Inject
constructor( constructor(
private val amneziaBackend: Provider<org.amnezia.awg.backend.Backend>, private val amneziaBackend: Provider<org.amnezia.awg.backend.Backend>,
tunnelConfigRepository: TunnelConfigRepository,
@Kernel private val kernelBackend: Provider<Backend>, @Kernel private val kernelBackend: Provider<Backend>,
private val appDataRepository: AppDataRepository, private val appDataRepository: AppDataRepository,
@ApplicationScope private val applicationScope: CoroutineScope, @ApplicationScope private val applicationScope: CoroutineScope,
@ -38,7 +39,22 @@ constructor(
) : TunnelService { ) : TunnelService {
private val _vpnState = MutableStateFlow(VpnState()) private val _vpnState = MutableStateFlow(VpnState())
override val vpnState: StateFlow<VpnState> = _vpnState.asStateFlow() override val vpnState: StateFlow<VpnState> = _vpnState.combine(
tunnelConfigRepository.getTunnelConfigsFlow(),
) {
vpnState, tunnels ->
vpnState.copy(
tunnelConfig = tunnels.firstOrNull { it.id == vpnState.tunnelConfig?.id },
)
}.stateIn(applicationScope, SharingStarted.Lazily, VpnState())
private var statsJob: Job? = null
private suspend fun backend(): Any {
val settings = appDataRepository.settings.getSettings()
if (settings.isKernelEnabled) return kernelBackend.get()
return amneziaBackend.get()
}
override suspend fun runningTunnelNames(): Set<String> { override suspend fun runningTunnelNames(): Set<String> {
return when (val backend = backend()) { return when (val backend = backend()) {
@ -48,8 +64,6 @@ constructor(
} }
} }
private var statsJob: Job? = null
private suspend fun setState(tunnelConfig: TunnelConfig, tunnelState: TunnelState): Result<TunnelState> { private suspend fun setState(tunnelConfig: TunnelConfig, tunnelState: TunnelState): Result<TunnelState> {
return runCatching { return runCatching {
when (val backend = backend()) { when (val backend = backend()) {
@ -73,33 +87,26 @@ constructor(
} }
} }
private suspend fun backend(): Any {
val settings = appDataRepository.settings.getSettings()
if (settings.isKernelEnabled) return kernelBackend.get()
return amneziaBackend.get()
}
override suspend fun startTunnel(tunnelConfig: TunnelConfig): Result<TunnelState> { override suspend fun startTunnel(tunnelConfig: TunnelConfig): Result<TunnelState> {
return withContext(ioDispatcher) { return withContext(ioDispatcher) {
if (_vpnState.value.status == TunnelState.UP) vpnState.value.tunnelConfig?.let { stopTunnel(it) } onBeforeStart(tunnelConfig)
emitTunnelConfig(tunnelConfig)
setState(tunnelConfig, TunnelState.UP).onSuccess { setState(tunnelConfig, TunnelState.UP).onSuccess {
emitTunnelState(it) emitTunnelState(it)
appDataRepository.tunnels.save(tunnelConfig.copy(isActive = true))
}.onFailure { }.onFailure {
Timber.e(it) Timber.e(it)
onStartFailed()
} }
} }
} }
override suspend fun stopTunnel(tunnelConfig: TunnelConfig): Result<TunnelState> { override suspend fun stopTunnel(tunnelConfig: TunnelConfig): Result<TunnelState> {
return withContext(ioDispatcher) { return withContext(ioDispatcher) {
onBeforeStop(tunnelConfig)
setState(tunnelConfig, TunnelState.DOWN).onSuccess { setState(tunnelConfig, TunnelState.DOWN).onSuccess {
emitTunnelState(it) emitTunnelState(it)
appDataRepository.tunnels.save(tunnelConfig.copy(isActive = false))
resetBackendStatistics()
}.onFailure { }.onFailure {
Timber.e(it) Timber.e(it)
onStopFailed()
} }
} }
} }
@ -107,7 +114,7 @@ constructor(
// use this when we just want to bounce tunnel and not change tunnelConfig active state // use this when we just want to bounce tunnel and not change tunnelConfig active state
override suspend fun bounceTunnel(tunnelConfig: TunnelConfig): Result<TunnelState> { override suspend fun bounceTunnel(tunnelConfig: TunnelConfig): Result<TunnelState> {
toggleTunnel(tunnelConfig) toggleTunnel(tunnelConfig)
delay(Constants.VPN_RESTART_DELAY) delay(VPN_RESTART_DELAY)
return toggleTunnel(tunnelConfig) return toggleTunnel(tunnelConfig)
} }
@ -122,6 +129,34 @@ constructor(
} }
} }
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()
}
private suspend fun onBeforeStart(tunnelConfig: TunnelConfig) {
if (_vpnState.value.status == TunnelState.UP) vpnState.value.tunnelConfig?.let { stopTunnel(it) }
resetBackendStatistics()
appDataRepository.tunnels.save(tunnelConfig.copy(isActive = true))
emitVpnStateConfig(tunnelConfig)
startStatsJob()
}
private suspend fun onBeforeStop(tunnelConfig: TunnelConfig) {
cancelStatsJob()
resetBackendStatistics()
appDataRepository.tunnels.save(tunnelConfig.copy(isActive = false))
}
private fun emitTunnelState(state: TunnelState) { private fun emitTunnelState(state: TunnelState) {
_vpnState.tryEmit( _vpnState.tryEmit(
_vpnState.value.copy( _vpnState.value.copy(
@ -138,7 +173,7 @@ constructor(
) )
} }
private fun emitTunnelConfig(tunnelConfig: TunnelConfig?) { private fun emitVpnStateConfig(tunnelConfig: TunnelConfig) {
_vpnState.tryEmit( _vpnState.tryEmit(
_vpnState.value.copy( _vpnState.value.copy(
tunnelConfig = tunnelConfig, tunnelConfig = tunnelConfig,
@ -174,21 +209,9 @@ constructor(
return _vpnState.value.tunnelConfig?.name ?: "" return _vpnState.value.tunnelConfig?.name ?: ""
} }
override fun onStateChange(newState: Tunnel.State) {
handleStateChange(TunnelState.from(newState))
}
private fun handleStateChange(state: TunnelState) {
emitTunnelState(state)
WireGuardAutoTunnel.instance.requestTunnelTileServiceStateUpdate()
when (state) {
TunnelState.UP -> startStatsJob()
else -> cancelStatsJob()
}
}
private fun startTunnelStatisticsJob() = applicationScope.launch(ioDispatcher) { private fun startTunnelStatisticsJob() = applicationScope.launch(ioDispatcher) {
val backend = backend() val backend = backend()
delay(STATS_START_DELAY)
while (true) { while (true) {
when (backend) { when (backend) {
is Backend -> emitBackendStatistics( is Backend -> emitBackendStatistics(
@ -202,11 +225,21 @@ constructor(
) )
} }
} }
delay(Constants.VPN_STATISTIC_CHECK_INTERVAL) delay(VPN_STATISTIC_CHECK_INTERVAL)
} }
} }
override fun onStateChange(newState: Tunnel.State) {
emitTunnelState(TunnelState.from(newState))
}
override fun onStateChange(state: State) { override fun onStateChange(state: State) {
handleStateChange(TunnelState.from(state)) emitTunnelState(TunnelState.from(state))
}
companion object {
const val STATS_START_DELAY = 5_000L
const val VPN_STATISTIC_CHECK_INTERVAL = 1_000L
const val VPN_RESTART_DELAY = 1_000L
} }
} }

View File

@ -145,7 +145,7 @@ fun ConfigScreen(tunnelId: Int, viewModel: ConfigViewModel, focusRequester: Focu
) )
} }
if(showApplicationsDialog) { if (showApplicationsDialog) {
ApplicationSelectionDialog(viewModel, uiState) { ApplicationSelectionDialog(viewModel, uiState) {
showApplicationsDialog = false showApplicationsDialog = false
} }

View File

@ -40,7 +40,6 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -96,7 +95,6 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState,
val haptic = LocalHapticFeedback.current val haptic = LocalHapticFeedback.current
val context = LocalContext.current val context = LocalContext.current
val snackbar = SnackbarController.current val snackbar = SnackbarController.current
val scope = rememberCoroutineScope()
var showBottomSheet by remember { mutableStateOf(false) } var showBottomSheet by remember { mutableStateOf(false) }
var showVpnPermissionDialog by remember { mutableStateOf(false) } var showVpnPermissionDialog by remember { mutableStateOf(false) }
@ -328,20 +326,22 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState,
uiState.tunnels, uiState.tunnels,
key = { tunnel -> tunnel.id }, key = { tunnel -> tunnel.id },
) { tunnel -> ) { tunnel ->
val isActive = uiState.tunnels.any { it.id == tunnel.id && it.isActive } val isActive = uiState.tunnels.any {
it.id == tunnel.id &&
it.isActive
}
val leadingIconColor = val leadingIconColor =
( (
if ( if (
isActive isActive && uiState.vpnState.statistics != null
) { ) {
uiState.vpnState.statistics uiState.vpnState.statistics.mapPeerStats()
?.mapPeerStats() .map { it.value?.handshakeStatus() }
?.map { it.value?.handshakeStatus() }
.let { statuses -> .let { statuses ->
when { when {
statuses?.all { it == HandshakeStatus.HEALTHY } == true -> mint statuses.all { it == HandshakeStatus.HEALTHY } -> mint
statuses?.any { it == HandshakeStatus.STALE } == true -> corn statuses.any { it == HandshakeStatus.STALE } -> corn
statuses?.all { it == HandshakeStatus.NOT_STARTED } == true -> statuses.all { it == HandshakeStatus.NOT_STARTED } ->
Color.Gray Color.Gray
else -> { else -> {
@ -381,15 +381,6 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState,
}, },
text = tunnel.name, text = tunnel.name,
onHold = { onHold = {
if (
(uiState.vpnState.status == TunnelState.UP) &&
(tunnel.name == uiState.vpnState.tunnelConfig?.name)
) {
snackbar.showMessage(
context.getString(R.string.turn_off_tunnel),
)
return@RowListItem
}
haptic.performHapticFeedback(HapticFeedbackType.LongPress) haptic.performHapticFeedback(HapticFeedbackType.LongPress)
selectedTunnel = tunnel selectedTunnel = tunnel
}, },
@ -416,18 +407,9 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState,
Row { Row {
IconButton( IconButton(
onClick = { onClick = {
if ( navController.navigate(
uiState.settings.isAutoTunnelEnabled && "${Screen.Option.route}/${selectedTunnel?.id}",
!uiState.settings.isAutoTunnelPaused )
) {
snackbar.showMessage(
context.getString(R.string.turn_off_tunnel),
)
} else {
navController.navigate(
"${Screen.Option.route}/${selectedTunnel?.id}",
)
}
}, },
) { ) {
val icon = Icons.Rounded.Settings val icon = Icons.Rounded.Settings
@ -444,6 +426,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState,
Icon(icon, icon.name) Icon(icon, icon.name)
} }
IconButton( IconButton(
enabled = !isActive,
modifier = Modifier.focusable(), modifier = Modifier.focusable(),
onClick = { showDeleteTunnelAlertDialog = true }, onClick = { showDeleteTunnelAlertDialog = true },
) { ) {
@ -468,16 +451,10 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState,
Row { Row {
IconButton( IconButton(
onClick = { onClick = {
if (uiState.settings.isAutoTunnelEnabled && !uiState.settings.isAutoTunnelPaused) { selectedTunnel = tunnel
snackbar.showMessage( navController.navigate(
context.getString(R.string.turn_off_auto), "${Screen.Option.route}/${selectedTunnel?.id}",
) )
} else {
selectedTunnel = tunnel
navController.navigate(
"${Screen.Option.route}/${selectedTunnel?.id}",
)
}
}, },
) { ) {
val icon = Icons.Rounded.Settings val icon = Icons.Rounded.Settings

View File

@ -56,9 +56,11 @@ import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle
import com.zaneschepke.wireguardautotunnel.ui.common.config.SubmitConfigurationTextBox import com.zaneschepke.wireguardautotunnel.ui.common.config.SubmitConfigurationTextBox
import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.ScrollDismissFab import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.ScrollDismissFab
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.WildcardSupportingLabel
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
import com.zaneschepke.wireguardautotunnel.util.extensions.isValidIpv4orIpv6Address import com.zaneschepke.wireguardautotunnel.util.extensions.isValidIpv4orIpv6Address
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@ -246,6 +248,7 @@ fun OptionsScreen(
value = currentText, value = currentText,
onValueChange = { currentText = it }, onValueChange = { currentText = it },
label = { Text(stringResource(id = R.string.use_tunnel_on_wifi_name)) }, label = { Text(stringResource(id = R.string.use_tunnel_on_wifi_name)) },
supportingText = { WildcardSupportingLabel { context.openWebUrl(it) } },
modifier = modifier =
Modifier Modifier
.padding( .padding(

View File

@ -314,7 +314,20 @@ fun SettingsScreen(
enabled = !uiState.settings.isAlwaysOnVpnEnabled, enabled = !uiState.settings.isAlwaysOnVpnEnabled,
checked = uiState.settings.isTunnelOnWifiEnabled, checked = uiState.settings.isTunnelOnWifiEnabled,
padding = screenPadding, padding = screenPadding,
onCheckChanged = { viewModel.onToggleTunnelOnWifi() }, onCheckChanged = { checked ->
if (!checked) viewModel.onToggleTunnelOnWifi()
if (checked) {
when (false) {
isBackgroundLocationGranted -> showLocationDialog = true
fineLocationState.status.isGranted -> showLocationDialog = true
viewModel.isLocationEnabled(context) ->
showLocationServicesAlertDialog = true
else -> {
viewModel.onToggleTunnelOnWifi()
}
}
}
},
modifier = modifier =
if (uiState.settings.isAutoTunnelEnabled) { if (uiState.settings.isAutoTunnelEnabled) {
Modifier Modifier
@ -442,23 +455,7 @@ fun SettingsScreen(
TextButton( TextButton(
onClick = { onClick = {
if (uiState.tunnels.isEmpty()) return@TextButton context.showToast(R.string.tunnel_required) if (uiState.tunnels.isEmpty()) return@TextButton context.showToast(R.string.tunnel_required)
if ( handleAutoTunnelToggle()
uiState.settings.isTunnelOnWifiEnabled &&
!uiState.settings.isAutoTunnelEnabled
) {
when (false) {
isBackgroundLocationGranted -> showLocationDialog = true
fineLocationState.status.isGranted -> showLocationDialog = true
viewModel.isLocationEnabled(context) ->
showLocationServicesAlertDialog = true
else -> {
handleAutoTunnelToggle()
}
}
} else {
handleAutoTunnelToggle()
}
}, },
) { ) {
val autoTunnelButtonText = val autoTunnelButtonText =

View File

@ -6,8 +6,8 @@ object Constants {
const val MANUAL_TUNNEL_CONFIG_ID = "0" const val MANUAL_TUNNEL_CONFIG_ID = "0"
const val BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT = 10 * 60 * 1_000L // 10 minutes const val BATTERY_SAVER_WATCHER_WAKE_LOCK_TIMEOUT = 10 * 60 * 1_000L // 10 minutes
const val VPN_STATISTIC_CHECK_INTERVAL = 1_000L
const val WATCHER_COLLECTION_DELAY = 3_000L const val WATCHER_COLLECTION_DELAY = 3_000L
const val CONF_FILE_EXTENSION = ".conf" const val CONF_FILE_EXTENSION = ".conf"
const val ZIP_FILE_EXTENSION = ".zip" const val ZIP_FILE_EXTENSION = ".zip"
const val URI_CONTENT_SCHEME = "content" const val URI_CONTENT_SCHEME = "content"
@ -27,12 +27,11 @@ object Constants {
const val DEFAULT_PING_IP = "1.1.1.1" const val DEFAULT_PING_IP = "1.1.1.1"
const val PING_TIMEOUT = 5_000L const val PING_TIMEOUT = 5_000L
const val VPN_RESTART_DELAY = 1_000L
const val PING_INTERVAL = 60_000L const val PING_INTERVAL = 60_000L
const val PING_COOLDOWN = PING_INTERVAL * 60 // one hour const val PING_COOLDOWN = PING_INTERVAL * 60 // one hour
const val UNREADABLE_SSID = "<unknown ssid>" const val UNREADABLE_SSID = "<unknown ssid>"
val amneziaProperties = listOf("Jc", "Jmin", "Jmax", "S1", "S2", "H1", "H2", "H3", "H4") val amProperties = listOf("Jc", "Jmin", "Jmax", "S1", "S2", "H1", "H2", "H3", "H4")
const val QR_CODE_NAME_PROPERTY = "# Name =" const val QR_CODE_NAME_PROPERTY = "# Name ="
} }

View File

@ -54,7 +54,7 @@ fun Config.toWgQuickString(): String {
val linesIterator = lines.iterator() val linesIterator = lines.iterator()
while (linesIterator.hasNext()) { while (linesIterator.hasNext()) {
val next = linesIterator.next() val next = linesIterator.next()
Constants.amneziaProperties.forEach { Constants.amProperties.forEach {
if (next.startsWith(it, ignoreCase = true)) { if (next.startsWith(it, ignoreCase = true)) {
linesIterator.remove() linesIterator.remove()
} }