fix: tunnel and auto-tunnel state sync

This commit is contained in:
zaneschepke
2026-05-24 05:09:33 -04:00
parent bf432cca0d
commit f83559f910
11 changed files with 110 additions and 34 deletions
@@ -10,6 +10,7 @@ import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.core.notification.AndroidNotificationService
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
import com.zaneschepke.wireguardautotunnel.core.service.tile.AutoTunnelTileRefresher
import com.zaneschepke.wireguardautotunnel.di.Dispatcher
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelActionSource
@@ -113,6 +114,7 @@ class AutoTunnelService : LifecycleService() {
fun start() {
stateHolder.setActive(true)
AutoTunnelTileRefresher.refresh(this)
launchWatcherNotification()
autoTunnelJob?.cancel()
autoTunnelJob = startAutoTunnelStateJob()
@@ -130,6 +132,7 @@ class AutoTunnelService : LifecycleService() {
override fun onDestroy() {
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
stateHolder.setActive(false)
AutoTunnelTileRefresher.refresh(this)
super.onDestroy()
}
@@ -7,6 +7,7 @@ import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelSta
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
@@ -18,11 +19,25 @@ class AutoTunnelControlTile : TileService() {
private val tileScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
override fun onDestroy() {
tileScope.cancel()
super.onDestroy()
}
override fun onStartListening() {
observeState()
super.onStartListening()
updateTileState()
startObserving()
}
override fun onTileAdded() {
super.onTileAdded()
updateTileState()
startObserving()
}
override fun onStopListening() {
super.onStopListening()
tileScope.coroutineContext.cancelChildren()
}
@@ -30,7 +45,12 @@ class AutoTunnelControlTile : TileService() {
unlockAndRun { tileScope.launch { autoTunnelCoordinator.toggle() } }
}
private fun observeState() {
private fun updateTileState() {
val isActive = autoTunnelStateHolder.active.value
if (isActive) setActive() else setInactive()
}
private fun startObserving() {
tileScope.launch {
autoTunnelStateHolder.active.collect { active ->
if (active) setActive() else setInactive()
@@ -0,0 +1,14 @@
package com.zaneschepke.wireguardautotunnel.core.service.tile
import android.content.ComponentName
import android.content.Context
import android.service.quicksettings.TileService
object AutoTunnelTileRefresher : TileRefresher {
override fun refresh(context: Context) {
TileService.requestListeningState(
context,
ComponentName(context, AutoTunnelControlTile::class.java),
)
}
}
@@ -0,0 +1,7 @@
package com.zaneschepke.wireguardautotunnel.core.service.tile
import android.content.Context
interface TileRefresher {
fun refresh(context: Context)
}
@@ -1,5 +1,6 @@
package com.zaneschepke.wireguardautotunnel.core.service.tile
import android.os.Build
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import com.zaneschepke.wireguardautotunnel.core.orchestration.TunnelCoordinator
@@ -27,8 +28,15 @@ class TunnelControlTile : TileService() {
super.onDestroy()
}
override fun onTileAdded() {
super.onTileAdded()
updateTileState()
startObserving()
}
override fun onStartListening() {
super.onStartListening()
updateTileState()
startObserving()
}
@@ -109,7 +117,12 @@ class TunnelControlTile : TileService() {
qsTile?.apply {
state = Tile.STATE_ACTIVE
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
subtitle = label
}
contentDescription = label
updateTile()
}
}
@@ -117,7 +130,12 @@ class TunnelControlTile : TileService() {
private fun setInactive() {
qsTile?.apply {
state = Tile.STATE_INACTIVE
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
subtitle = ""
}
contentDescription = ""
updateTile()
}
}
@@ -125,7 +143,12 @@ class TunnelControlTile : TileService() {
private fun setUnavailable() {
qsTile?.apply {
state = Tile.STATE_UNAVAILABLE
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
subtitle = ""
}
contentDescription = ""
updateTile()
}
}
@@ -0,0 +1,14 @@
package com.zaneschepke.wireguardautotunnel.core.service.tile
import android.content.ComponentName
import android.content.Context
import android.service.quicksettings.TileService
object TunnelTileRefresher : TileRefresher {
override fun refresh(context: Context) {
TileService.requestListeningState(
context,
ComponentName(context, TunnelControlTile::class.java),
)
}
}
@@ -1,6 +1,7 @@
package com.zaneschepke.wireguardautotunnel.di
import android.app.Notification
import android.content.Context
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.networkmonitor.StableNetworkEngine
@@ -15,6 +16,7 @@ import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.PROXY_GROUP_KEY
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationService.Companion.VPN_GROUP_KEY
import com.zaneschepke.wireguardautotunnel.core.notification.TunnelNotificationService
import com.zaneschepke.wireguardautotunnel.core.service.tile.TunnelTileRefresher
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelBackendProvider
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsRepository
@@ -62,6 +64,10 @@ val tunnelBackendProviderModule = module {
override val proxyNotificationId: Int
get() = NotificationService.PROXY_NOTIFICATION_ID
override fun refreshTile(context: Context) {
TunnelTileRefresher.refresh(context)
}
}
}
@@ -1,10 +1,13 @@
package com.zaneschepke.tunnel
import android.app.Notification
import android.content.Context
interface NotificationProvider {
val vpnInitNotification: Notification
val proxyInitNotification: Notification
val vpnNotificationId: Int
val proxyNotificationId: Int
fun refreshTile(context: Context)
}
@@ -18,7 +18,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import timber.log.Timber
internal class ServiceHolder(private val context: Context) {
internal class ServiceHolder(val context: Context) {
internal val uapiPath = context.dataDir.absolutePath
@@ -58,6 +58,8 @@ class TunnelBackend(
var hadVpnTunnels = false
var hadProxyTunnels = false
var lastActiveTunnelIds: Set<Int> = emptySet()
actor.state.collect { actorState ->
val hasVpnNow =
actorState.byTunnelId.values.any { it.running.mode is BackendMode.Vpn }
@@ -69,6 +71,14 @@ class TunnelBackend(
_status.update { current -> current.copy(activeTunnels = activeTunnels) }
val currentTunnelIds = activeTunnels.keys
// update tile
if (currentTunnelIds != lastActiveTunnelIds) {
notificationProvider.refreshTile(serviceHolder.context)
lastActiveTunnelIds = currentTunnelIds
}
// VPN cleanup
if (hadVpnTunnels && !hasVpnNow) {
@@ -281,10 +291,14 @@ class TunnelBackend(
override fun emergencyStopAllOfTypeSync(modeClass: KClass<out BackendMode>) {
actor.emergencyStopAllOfType(modeClass)
_status.update { it.copy(activeTunnels = emptyMap()) }
notificationProvider.refreshTile(serviceHolder.context)
}
override suspend fun stopAllActiveTunnels(): Result<Unit> = runCatching {
_status.value.activeTunnels.forEach { (id, _) -> stop(id) }
_status.update { it.copy(activeTunnels = emptyMap()) }
notificationProvider.refreshTile(serviceHolder.context)
}
private fun startSystemDnsMonitoring() {
@@ -1,28 +0,0 @@
package com.zaneschepke.tunnel.backend
import com.zaneschepke.tunnel.StatusCallback
import com.zaneschepke.tunnel.VpnBackend
import com.zaneschepke.tunnel.state.NativeTunnelStatus
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.receiveAsFlow
object TunnelEventBus {
private val channel = Channel<NativeTunnelStatus>(Channel.BUFFERED)
val flow = channel.receiveAsFlow()
private val callback = StatusCallback { handle, code ->
val status = NativeTunnelStatus.NativeTunnelStatusCode.from(code) ?: return@StatusCallback
channel.trySend(NativeTunnelStatus(handle = handle, code = status))
}
fun start() {
VpnBackend.setStatusCallback(callback)
}
fun stop() {
VpnBackend.setStatusCallback(null)
channel.close()
}
}