parent
64a7680b81
commit
70649383e0
|
@ -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"
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
package com.zaneschepke.wireguardautotunnel.service.foreground
|
||||
|
||||
enum class Action {
|
||||
START,
|
||||
START_FOREGROUND,
|
||||
STOP,
|
||||
STOP_FOREGROUND,
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue