Compare commits

...

1 Commits

Author SHA1 Message Date
Zane Schepke 5e4fcdc634 refactor: switch to bound services 2025-04-26 03:31:02 -04:00
9 changed files with 99 additions and 76 deletions
@@ -39,7 +39,9 @@ class RestartReceiver : BroadcastReceiver() {
applicationScope.launch(ioDispatcher) {
val settings = appDataRepository.settings.get()
if (settings.isRestoreOnBootEnabled) {
if (settings.isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) {
if (
settings.isAutoTunnelEnabled && serviceManager.autoTunnelService.value == null
) {
Timber.d("Starting auto-tunnel on boot/update")
serviceManager.startAutoTunnel()
} else {
@@ -1,10 +1,11 @@
package com.zaneschepke.wireguardautotunnel.core.service
import android.app.Service
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.net.VpnService
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
import android.os.IBinder
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
@@ -13,7 +14,6 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import jakarta.inject.Inject
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
@@ -37,23 +37,37 @@ constructor(
private val autoTunnelMutex = Mutex()
private val _autoTunnelActive = MutableStateFlow(false)
val autoTunnelActive = _autoTunnelActive.asStateFlow()
private val _tunnelService = MutableStateFlow<TunnelForegroundService?>(null)
private val _autoTunnelService = MutableStateFlow<AutoTunnelService?>(null)
val autoTunnelService = _autoTunnelService.asStateFlow()
var autoTunnelService = CompletableDeferred<AutoTunnelService>()
var backgroundService = CompletableDeferred<TunnelForegroundService>()
private fun <T : Service> startService(cls: Class<T>, background: Boolean) {
runCatching {
val intent = Intent(context, cls)
if (background) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
private val tunnelServiceConnection =
object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
val binder = service as? TunnelForegroundService.LocalBinder
_tunnelService.value = binder?.service
Timber.d("TunnelForegroundService connected")
}
.onFailure { Timber.e(it) }
}
override fun onServiceDisconnected(name: ComponentName) {
_tunnelService.value = null
Timber.d("TunnelForegroundService disconnected")
}
}
private val autoTunnelServiceConnection =
object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
val binder = service as? AutoTunnelService.LocalBinder
_autoTunnelService.value = binder?.service
Timber.d("AutoTunnelService connected")
}
override fun onServiceDisconnected(name: ComponentName) {
_autoTunnelService.value = null
Timber.d("AutoTunnelService disconnected")
}
}
fun hasVpnPermission(): Boolean {
return VpnService.prepare(context) == null
@@ -63,20 +77,13 @@ constructor(
autoTunnelMutex.withLock {
val settings = appDataRepository.settings.get()
appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = true))
if (autoTunnelService.isCompleted) {
_autoTunnelActive.update { true }
return
if (_autoTunnelService.value != null) return
withContext(ioDispatcher) {
val intent = Intent(context, AutoTunnelService::class.java)
context.startForegroundService(intent)
context.bindService(intent, autoTunnelServiceConnection, Context.BIND_AUTO_CREATE)
withContext(mainDispatcher) { updateAutoTunnelTile() }
}
runCatching {
autoTunnelService = CompletableDeferred()
startService(AutoTunnelService::class.java, !WireGuardAutoTunnel.isForeground())
_autoTunnelActive.update { true }
}
.onFailure {
Timber.e(it)
_autoTunnelActive.update { false }
}
withContext(mainDispatcher) { updateAutoTunnelTile() }
}
}
@@ -84,43 +91,44 @@ constructor(
autoTunnelMutex.withLock {
val settings = appDataRepository.settings.get()
appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = false))
if (!autoTunnelService.isCompleted) return
runCatching {
val service = autoTunnelService.await()
service.stop()
_autoTunnelActive.update { false }
autoTunnelService = CompletableDeferred()
if (_autoTunnelService.value == null) return
_autoTunnelService.value?.let { service ->
service.stop()
try {
context.unbindService(autoTunnelServiceConnection)
} finally {
_tunnelService.value = null
}
.onFailure { Timber.e(it) }
}
withContext(mainDispatcher) { updateAutoTunnelTile() }
}
}
fun startTunnelForegroundService() {
if (backgroundService.isCompleted) return
runCatching {
backgroundService = CompletableDeferred()
startService(
TunnelForegroundService::class.java,
!WireGuardAutoTunnel.isForeground(),
)
suspend fun startTunnelForegroundService() {
if (_tunnelService.value != null) return
withContext(ioDispatcher) {
applicationScope.launch(ioDispatcher) {
val intent = Intent(context, TunnelForegroundService::class.java)
context.startForegroundService(intent)
context.bindService(intent, tunnelServiceConnection, Context.BIND_AUTO_CREATE)
}
.onFailure { Timber.e(it) }
}
}
suspend fun stopTunnelForegroundService() {
if (!backgroundService.isCompleted) return
runCatching {
val service = backgroundService.await()
service.stop()
backgroundService = CompletableDeferred()
fun stopTunnelForegroundService() {
_tunnelService.value?.let { service ->
service.stop()
try {
context.unbindService(tunnelServiceConnection)
} finally {
_tunnelService.value = null
}
.onFailure { Timber.e(it) }
}
}
fun toggleAutoTunnel() {
applicationScope.launch(ioDispatcher) {
if (_autoTunnelActive.value) stopAutoTunnel() else startAutoTunnel()
if (_autoTunnelService.value != null) stopAutoTunnel() else startAutoTunnel()
}
}
@@ -131,4 +139,12 @@ constructor(
fun updateTunnelTile() {
context.requestTunnelTileServiceStateUpdate()
}
fun handleTunnelServiceDestroy() {
_tunnelService.update { null }
}
fun handleAutoTunnelServiceDestroy() {
_autoTunnelService.update { null }
}
}
@@ -2,6 +2,7 @@ package com.zaneschepke.wireguardautotunnel.core.service
import android.app.Notification
import android.content.Intent
import android.os.Binder
import android.os.IBinder
import androidx.core.app.ServiceCompat
import androidx.lifecycle.LifecycleService
@@ -23,7 +24,6 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.distinctByKeys
import dagger.hilt.android.AndroidEntryPoint
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Job
import kotlinx.coroutines.NonCancellable
@@ -64,9 +64,12 @@ class TunnelForegroundService : LifecycleService() {
private val jobsMutex = Mutex()
class LocalBinder(val service: TunnelForegroundService) : Binder()
private val binder = LocalBinder(this)
override fun onCreate() {
super.onCreate()
serviceManager.backgroundService.complete(this)
ServiceCompat.startForeground(
this@TunnelForegroundService,
NotificationManager.VPN_NOTIFICATION_ID,
@@ -75,14 +78,13 @@ class TunnelForegroundService : LifecycleService() {
)
}
override fun onBind(intent: Intent): IBinder? {
override fun onBind(intent: Intent): IBinder {
super.onBind(intent)
return null
return binder
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
serviceManager.backgroundService.complete(this)
ServiceCompat.startForeground(
this@TunnelForegroundService,
NotificationManager.VPN_NOTIFICATION_ID,
@@ -273,7 +275,7 @@ class TunnelForegroundService : LifecycleService() {
}
override fun onDestroy() {
serviceManager.backgroundService = CompletableDeferred()
serviceManager.handleTunnelServiceDestroy()
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
super.onDestroy()
}
@@ -1,6 +1,7 @@
package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
import android.content.Intent
import android.os.Binder
import android.os.IBinder
import android.os.PowerManager
import androidx.core.app.ServiceCompat
@@ -28,7 +29,6 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import javax.inject.Provider
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
@@ -68,21 +68,23 @@ class AutoTunnelService : LifecycleService() {
private var killSwitchJob: Job? = null
class LocalBinder(val service: AutoTunnelService) : Binder()
private val binder = LocalBinder(this)
override fun onCreate() {
super.onCreate()
serviceManager.autoTunnelService.complete(this)
launchWatcherNotification()
}
override fun onBind(intent: Intent): IBinder? {
override fun onBind(intent: Intent): IBinder {
super.onBind(intent)
return null
return binder
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
Timber.d("onStartCommand executed with startId: $startId")
serviceManager.autoTunnelService.complete(this)
start()
return START_STICKY
}
@@ -105,7 +107,7 @@ class AutoTunnelService : LifecycleService() {
}
override fun onDestroy() {
serviceManager.autoTunnelService = CompletableDeferred()
serviceManager.handleAutoTunnelServiceDestroy()
restoreVpnKillSwitch()
super.onDestroy()
}
@@ -38,8 +38,8 @@ class AutoTunnelControlTile : TileService(), LifecycleOwner {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
Timber.d("Start listening called for auto tunnel tile")
lifecycleScope.launch {
serviceManager.autoTunnelActive.collect {
if (it) return@collect setActive()
serviceManager.autoTunnelService.collect {
if (it != null) return@collect setActive()
setInactive()
}
}
@@ -56,7 +56,7 @@ class AutoTunnelControlTile : TileService(), LifecycleOwner {
super.onClick()
unlockAndRun {
lifecycleScope.launch {
if (serviceManager.autoTunnelActive.value) {
if (serviceManager.autoTunnelService.value != null) {
serviceManager.stopAutoTunnel()
setInactive()
} else {
@@ -193,15 +193,16 @@ abstract class BaseTunnel(
tunThreads[tunnel.id]?.let {
if (it.state != Thread.State.TERMINATED) {
it.interrupt()
updateTunnelStatus(tunnel, TunnelStatus.Down)
} else {
Timber.d("Thread already terminated")
}
}
} catch (e: Exception) {
Timber.e(e, "Failed to stop tunnel thread for ${tunnel.name}")
} finally {
updateTunnelStatus(tunnel, TunnelStatus.Down)
cleanUpTunThread(tunnel)
}
cleanUpTunThread(tunnel)
}
private fun cleanUpTunThread(tunnel: TunnelConf) {
@@ -69,7 +69,7 @@ constructor(
// restore vpn kill switch if needed
private fun handlePreviouslyEnabledVpnKillSwitch() {
// let auto tunnel handle this if it is active
if (!serviceManager.autoTunnelActive.value) {
if (serviceManager.autoTunnelService.value == null) {
previousBackendState?.let { (state, lanEnabled) ->
Timber.d("Restoring kill switch configuration")
val lan = if (lanEnabled) TunnelConf.LAN_BYPASS_ALLOWED_IPS else emptyList()
@@ -57,7 +57,7 @@ constructor(
withContext(ioDispatcher) {
Timber.i("Service worker started")
with(appDataRepository.settings.get()) {
if (isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value)
if (isAutoTunnelEnabled && serviceManager.autoTunnelService.value == null)
return@with serviceManager.startAutoTunnel()
if (tunnelManager.activeTunnels.value.isEmpty())
tunnelManager.restorePreviousState()
@@ -86,7 +86,7 @@ constructor(
appDataRepository.tunnels.flow,
appDataRepository.appState.flow,
tunnelManager.activeTunnels,
serviceManager.autoTunnelActive,
serviceManager.autoTunnelService.map { it != null },
networkMonitor.networkStatusFlow,
) { array ->
val settings = array[0] as AppSettings