mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
fix: improve tunnel display states and transitions
This commit is contained in:
@@ -98,6 +98,7 @@ class WireGuardAutoTunnel : Application(), KoinComponent {
|
||||
provider.events,
|
||||
provider.backendStatus,
|
||||
coordinator.errors,
|
||||
tunnelCoordinator.tunnelDisplayStates,
|
||||
)
|
||||
|
||||
applicationScope.launch(ioDispatcher) { boostrapCoordinator.bootstrap() }
|
||||
|
||||
+17
-16
@@ -29,6 +29,7 @@ class TunnelEventDispatcher(
|
||||
providerEvents: Flow<TunnelEvent>,
|
||||
providerStatus: StateFlow<BackendStatus>,
|
||||
coordinatorErrors: Flow<TunnelErrorEvent>,
|
||||
tunnelDisplayStates: StateFlow<Map<Int, DisplayTunnelState>>,
|
||||
) {
|
||||
|
||||
// informational events
|
||||
@@ -85,22 +86,23 @@ class TunnelEventDispatcher(
|
||||
.launchIn(scope)
|
||||
|
||||
// vpn
|
||||
combine(providerStatus.map { it.activeTunnels }, tunnelRepository.userTunnelsFlow) {
|
||||
activeTunnels,
|
||||
allTunnels ->
|
||||
combine(
|
||||
providerStatus.map { it.activeTunnels },
|
||||
tunnelRepository.userTunnelsFlow,
|
||||
tunnelDisplayStates,
|
||||
) { activeTunnels, allTunnels, displayStates ->
|
||||
activeTunnels
|
||||
.mapNotNull { (id, activeTunnel) ->
|
||||
val mode = activeTunnel.mode ?: return@mapNotNull null
|
||||
|
||||
// Only include VPN / KillSwitchPrimary modes
|
||||
if (
|
||||
mode !is BackendMode.Vpn && mode !is BackendMode.Proxy.KillSwitchPrimary
|
||||
) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
|
||||
val tunnel = allTunnels.find { it.id == id } ?: return@mapNotNull null
|
||||
val displayState = DisplayTunnelState.from(activeTunnel)
|
||||
|
||||
val displayState =
|
||||
displayStates[id] ?: DisplayTunnelState.from(activeTunnel)
|
||||
|
||||
TunnelNotificationLine(
|
||||
id = id,
|
||||
@@ -115,20 +117,19 @@ class TunnelEventDispatcher(
|
||||
.launchIn(scope)
|
||||
|
||||
// proxy
|
||||
combine(providerStatus.map { it.activeTunnels }, tunnelRepository.userTunnelsFlow) {
|
||||
activeTunnels,
|
||||
allTunnels ->
|
||||
combine(
|
||||
providerStatus.map { it.activeTunnels },
|
||||
tunnelRepository.userTunnelsFlow,
|
||||
tunnelDisplayStates,
|
||||
) { activeTunnels, allTunnels, displayStates ->
|
||||
activeTunnels
|
||||
.mapNotNull { (id, activeTunnel) ->
|
||||
val mode = activeTunnel.mode ?: return@mapNotNull null
|
||||
|
||||
// Only include Standard Proxy mode
|
||||
if (mode !is BackendMode.Proxy.Standard) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
if (mode !is BackendMode.Proxy.Standard) return@mapNotNull null
|
||||
|
||||
val tunnel = allTunnels.find { it.id == id } ?: return@mapNotNull null
|
||||
val displayState = DisplayTunnelState.from(activeTunnel)
|
||||
val displayState =
|
||||
displayStates[id] ?: DisplayTunnelState.from(activeTunnel)
|
||||
|
||||
TunnelNotificationLine(
|
||||
id = id,
|
||||
|
||||
+17
@@ -17,14 +17,20 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepos
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.MonitoringSettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.ProxySettingsRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.DisplayTunnelState
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
@@ -41,6 +47,17 @@ class TunnelCoordinator(
|
||||
scope: CoroutineScope,
|
||||
) {
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
val tunnelDisplayStates: StateFlow<Map<Int, DisplayTunnelState>> =
|
||||
tunnelProvider.backendStatus
|
||||
.map { status ->
|
||||
status.activeTunnels.mapValues { (_, activeTunnel) ->
|
||||
DisplayTunnelState.from(activeTunnel)
|
||||
}
|
||||
}
|
||||
.debounce(400L.milliseconds)
|
||||
.stateIn(scope = scope, started = SharingStarted.Eagerly, initialValue = emptyMap())
|
||||
|
||||
data class RuntimeSettingsSnapshot(
|
||||
val general: GeneralSettings,
|
||||
val dns: DnsSettings,
|
||||
|
||||
+4
-3
@@ -87,15 +87,16 @@ fun TunnelList(
|
||||
uiState.backendStatus.activeTunnels[tunnel.id] ?: ActiveTunnel()
|
||||
}
|
||||
|
||||
val displayState = remember(activeTunnel) { DisplayTunnelState.from(activeTunnel) }
|
||||
val displayState =
|
||||
uiState.displayStates[tunnel.id] ?: DisplayTunnelState.from(activeTunnel)
|
||||
|
||||
val isRunning = uiState.backendStatus.activeTunnels.containsKey(tunnel.id)
|
||||
|
||||
val selected =
|
||||
remember(uiState.selectedTunnels) {
|
||||
uiState.selectedTunnels.any { it.id == tunnel.id }
|
||||
}
|
||||
|
||||
val isRunning = displayState != DisplayTunnelState.Disconnected
|
||||
|
||||
SurfaceRow(
|
||||
modifier =
|
||||
Modifier.animateItem()
|
||||
|
||||
+53
-20
@@ -4,6 +4,7 @@ import android.content.Context
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.zaneschepke.tunnel.Tunnel
|
||||
import com.zaneschepke.tunnel.model.BackendMode
|
||||
import com.zaneschepke.tunnel.state.ActiveTunnel
|
||||
import com.zaneschepke.tunnel.state.BootstrapState
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
@@ -13,42 +14,49 @@ import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.Straw
|
||||
|
||||
sealed class DisplayTunnelState {
|
||||
data object Disconnected : DisplayTunnelState()
|
||||
|
||||
data object Connecting : DisplayTunnelState()
|
||||
|
||||
data object ResolvingDns : DisplayTunnelState()
|
||||
|
||||
data object Connected : DisplayTunnelState()
|
||||
data object EstablishingConnection : DisplayTunnelState()
|
||||
|
||||
data object Ready : DisplayTunnelState()
|
||||
|
||||
data object Degraded : DisplayTunnelState()
|
||||
data object Connected : DisplayTunnelState()
|
||||
|
||||
data object Disconnected : DisplayTunnelState()
|
||||
data object Degraded : DisplayTunnelState()
|
||||
|
||||
@StringRes
|
||||
fun labelRes(): Int {
|
||||
return when (this) {
|
||||
ResolvingDns -> R.string.tunnel_state_resolving_dns
|
||||
Disconnected -> R.string.tunnel_state_disconnected
|
||||
Connecting -> R.string.tunnel_state_starting
|
||||
ResolvingDns -> R.string.tunnel_state_resolving_dns
|
||||
EstablishingConnection -> R.string.tunnel_state_establishing_connection
|
||||
Ready -> R.string.ready
|
||||
Connected -> R.string.tunnel_state_connected
|
||||
Degraded -> R.string.tunnel_state_handshake_failure
|
||||
Disconnected -> R.string.tunnel_state_disconnected
|
||||
Ready -> R.string.ready
|
||||
}
|
||||
}
|
||||
|
||||
fun asLocalizedString(context: Context): String {
|
||||
return context.getString(this.labelRes())
|
||||
return context.getString(labelRes())
|
||||
}
|
||||
|
||||
fun asColor(): Color {
|
||||
return when (this) {
|
||||
Disconnected -> CoolGray
|
||||
|
||||
Connecting,
|
||||
Ready,
|
||||
ResolvingDns -> Straw
|
||||
Degraded -> AlertRed
|
||||
ResolvingDns,
|
||||
EstablishingConnection,
|
||||
Ready -> Straw
|
||||
|
||||
Connected -> SilverTree
|
||||
|
||||
Degraded -> AlertRed
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,25 +64,50 @@ sealed class DisplayTunnelState {
|
||||
fun from(activeTunnel: ActiveTunnel): DisplayTunnelState {
|
||||
val transport = activeTunnel.transportState
|
||||
val bootstrap = activeTunnel.bootstrapState
|
||||
val mode = activeTunnel.mode
|
||||
val isVpnStyle = mode is BackendMode.Vpn || mode is BackendMode.Proxy.KillSwitchPrimary
|
||||
|
||||
// Static peers bootstrap never goes to complete, treat none the same
|
||||
val bootstrapPhaseDone =
|
||||
bootstrap is BootstrapState.Complete || bootstrap is BootstrapState.None
|
||||
|
||||
return when {
|
||||
transport is Tunnel.State.Down -> Disconnected
|
||||
|
||||
(bootstrap is BootstrapState.Complete &&
|
||||
!activeTunnel.isPeerUpdating &&
|
||||
transport is Tunnel.State.Starting) ||
|
||||
bootstrap is BootstrapState.None && transport is Tunnel.State.Starting -> Ready
|
||||
bootstrap is BootstrapState.Failed -> Degraded
|
||||
|
||||
// DNS resolution still in progress
|
||||
bootstrap is BootstrapState.ResolvingDns ||
|
||||
(bootstrap is BootstrapState.Complete && activeTunnel.isPeerUpdating) ->
|
||||
ResolvingDns
|
||||
|
||||
transport is Tunnel.State.Starting -> Connecting
|
||||
bootstrap is BootstrapState.UpdatingPeers -> ResolvingDns
|
||||
|
||||
transport is Tunnel.State.Up.Healthy -> Connected
|
||||
|
||||
transport is Tunnel.State.Up.HandshakeFailure && !activeTunnel.isPeerUpdating ->
|
||||
Degraded
|
||||
transport is Tunnel.State.Up.HandshakeFailure -> {
|
||||
val age = System.currentTimeMillis() - activeTunnel.lastStateChangeMs
|
||||
|
||||
if (age > 15_000L && bootstrapPhaseDone) {
|
||||
Degraded
|
||||
} else if (isVpnStyle && bootstrapPhaseDone) {
|
||||
EstablishingConnection
|
||||
} else if (bootstrapPhaseDone) {
|
||||
// For regular proxy mode, we go to ready once past bootstrap phase
|
||||
Ready
|
||||
} else {
|
||||
Connecting
|
||||
}
|
||||
}
|
||||
|
||||
transport is Tunnel.State.Starting -> {
|
||||
when {
|
||||
bootstrapPhaseDone -> {
|
||||
if (isVpnStyle) EstablishingConnection else Ready
|
||||
}
|
||||
else -> Connecting
|
||||
}
|
||||
}
|
||||
|
||||
// Final fallback after bootstrap phase is done
|
||||
bootstrapPhaseDone -> if (isVpnStyle) EstablishingConnection else Ready
|
||||
|
||||
else -> Connecting
|
||||
}
|
||||
|
||||
@@ -7,5 +7,6 @@ data class TunnelsUiState(
|
||||
val tunnels: List<TunnelConfig> = emptyList(),
|
||||
val backendStatus: BackendStatus = BackendStatus(),
|
||||
val selectedTunnels: List<TunnelConfig> = emptyList(),
|
||||
val displayStates: Map<Int, DisplayTunnelState> = emptyMap(),
|
||||
val isLoading: Boolean = true,
|
||||
)
|
||||
|
||||
+3
-1
@@ -71,10 +71,12 @@ class SharedAppViewModel(
|
||||
tunnelRepository.userTunnelsFlow,
|
||||
tunnelCoordinator.backendStatus,
|
||||
selectedTunnelsRepository.flow,
|
||||
) { tunnels, backendStatus, selectedTuns ->
|
||||
tunnelCoordinator.tunnelDisplayStates,
|
||||
) { tunnels, backendStatus, selectedTuns, displayStates ->
|
||||
TunnelsUiState(
|
||||
tunnels = tunnels,
|
||||
backendStatus = backendStatus,
|
||||
displayStates = displayStates,
|
||||
selectedTunnels = selectedTuns,
|
||||
isLoading = false,
|
||||
)
|
||||
|
||||
@@ -580,4 +580,5 @@
|
||||
<string name="app">App</string>
|
||||
<string name="app_channel_description">A channel for general application notifications, like version updates</string>
|
||||
<string name="stop_all">Stop all</string>
|
||||
<string name="tunnel_state_establishing_connection">Establishing connection</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user