Compare commits

...

3 Commits

Author SHA1 Message Date
Zane Schepke 2fc0a1a394 fix: remove screen on intent 2025-04-28 15:04:24 -04:00
Zane Schepke afb3014c49 fix: start up bug (logger)
fix: error handling
refactor: expose resolved endpoint
2025-04-28 14:54:29 -04:00
Zane Schepke 5e4fcdc634 refactor: switch to bound services 2025-04-26 03:31:02 -04:00
21 changed files with 246 additions and 187 deletions
+1 -4
View File
@@ -166,15 +166,12 @@
<receiver <receiver
android:name=".core.broadcast.RestartReceiver" android:name=".core.broadcast.RestartReceiver"
android:enabled="true" android:enabled="true"
android:exported="false" android:exported="false">
android:directBootAware="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SCREEN_ON" />
<action android:name="android.intent.action.USER_PRESENT" /> <action android:name="android.intent.action.USER_PRESENT" />
<action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" /> <action android:name="android.intent.action.QUICKBOOT_POWERON" />
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" /> <action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" /> <action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter> </intent-filter>
</receiver> </receiver>
@@ -39,7 +39,6 @@ import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute import androidx.navigation.toRoute
import com.zaneschepke.networkmonitor.NetworkMonitor import com.zaneschepke.networkmonitor.NetworkMonitor
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
import com.zaneschepke.wireguardautotunnel.ui.Route import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.VpnDeniedDialog import com.zaneschepke.wireguardautotunnel.ui.common.dialog.VpnDeniedDialog
@@ -110,6 +109,7 @@ class MainActivity : AppCompatActivity() {
val isTv = isRunningOnTv() val isTv = isRunningOnTv()
val appUiState by viewModel.uiState.collectAsStateWithLifecycle() val appUiState by viewModel.uiState.collectAsStateWithLifecycle()
val appViewState by viewModel.appViewState.collectAsStateWithLifecycle() val appViewState by viewModel.appViewState.collectAsStateWithLifecycle()
val tunnelError by viewModel.tunnelManager.errorEvents.collectAsStateWithLifecycle(null)
val navController = rememberNavController() val navController = rememberNavController()
val backStackEntry by navController.currentBackStackEntryAsState() val backStackEntry by navController.currentBackStackEntryAsState()
@@ -151,6 +151,15 @@ class MainActivity : AppCompatActivity() {
viewModel.handleEvent(AppEvent.SetBatteryOptimizeDisableShown) viewModel.handleEvent(AppEvent.SetBatteryOptimizeDisableShown)
} }
LaunchedEffect(tunnelError) {
if (tunnelError == null) return@LaunchedEffect
val message = tunnelError!!.second.toStringRes()
val context = this@MainActivity
snackbar.showSnackbar(
context.getString(R.string.tunnel_error_template, context.getString(message))
)
}
with(appViewState) { with(appViewState) {
LaunchedEffect(isConfigChanged) { LaunchedEffect(isConfigChanged) {
if (isConfigChanged) { if (isConfigChanged) {
@@ -166,21 +175,6 @@ class MainActivity : AppCompatActivity() {
viewModel.handleEvent(AppEvent.MessageShown) viewModel.handleEvent(AppEvent.MessageShown)
} }
} }
LaunchedEffect(appUiState.activeTunnels) {
appUiState.activeTunnels.mapNotNull { (tunnelConf, tunnelState) ->
(tunnelState.status as? TunnelStatus.Error)?.let { error ->
val message = error.error.toStringRes()
val context = this@MainActivity
snackbar.showSnackbar(
context.getString(
R.string.tunnel_error_template,
context.getString(message),
)
)
viewModel.handleEvent(AppEvent.ClearTunnelError(tunnelConf))
}
}
}
LaunchedEffect(popBackStack) { LaunchedEffect(popBackStack) {
if (popBackStack) { if (popBackStack) {
navController.popBackStack() navController.popBackStack()
@@ -32,14 +32,15 @@ class RestartReceiver : BroadcastReceiver() {
Timber.d("RestartReceiver triggered with action: ${intent.action}") Timber.d("RestartReceiver triggered with action: ${intent.action}")
// screen on for Android TV only to help with sleep shutdowns // screen on for Android TV only to help with sleep shutdowns
val isTv = context.isRunningOnTv() val isTv = context.isRunningOnTv()
if (intent.action == Intent.ACTION_SCREEN_ON && !isTv) return
if (intent.action == Intent.ACTION_USER_PRESENT && !isTv) return if (intent.action == Intent.ACTION_USER_PRESENT && !isTv) return
serviceManager.updateTunnelTile() serviceManager.updateTunnelTile()
serviceManager.updateAutoTunnelTile() serviceManager.updateAutoTunnelTile()
applicationScope.launch(ioDispatcher) { applicationScope.launch(ioDispatcher) {
val settings = appDataRepository.settings.get() val settings = appDataRepository.settings.get()
if (settings.isRestoreOnBootEnabled) { if (settings.isRestoreOnBootEnabled) {
if (settings.isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) { if (
settings.isAutoTunnelEnabled && serviceManager.autoTunnelService.value == null
) {
Timber.d("Starting auto-tunnel on boot/update") Timber.d("Starting auto-tunnel on boot/update")
serviceManager.startAutoTunnel() serviceManager.startAutoTunnel()
} else { } else {
@@ -1,10 +1,11 @@
package com.zaneschepke.wireguardautotunnel.core.service package com.zaneschepke.wireguardautotunnel.core.service
import android.app.Service import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.ServiceConnection
import android.net.VpnService 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.core.service.autotunnel.AutoTunnelService
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher 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.requestAutoTunnelTileServiceUpdate
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
import jakarta.inject.Inject import jakarta.inject.Inject
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@@ -37,23 +37,37 @@ constructor(
private val autoTunnelMutex = Mutex() private val autoTunnelMutex = Mutex()
private val _autoTunnelActive = MutableStateFlow(false) private val _tunnelService = MutableStateFlow<TunnelForegroundService?>(null)
val autoTunnelActive = _autoTunnelActive.asStateFlow() private val _autoTunnelService = MutableStateFlow<AutoTunnelService?>(null)
val autoTunnelService = _autoTunnelService.asStateFlow()
var autoTunnelService = CompletableDeferred<AutoTunnelService>() private val tunnelServiceConnection =
var backgroundService = CompletableDeferred<TunnelForegroundService>() object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
private fun <T : Service> startService(cls: Class<T>, background: Boolean) { val binder = service as? TunnelForegroundService.LocalBinder
runCatching { _tunnelService.value = binder?.service
val intent = Intent(context, cls) Timber.d("TunnelForegroundService connected")
if (background) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
} }
.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 { fun hasVpnPermission(): Boolean {
return VpnService.prepare(context) == null return VpnService.prepare(context) == null
@@ -63,20 +77,13 @@ constructor(
autoTunnelMutex.withLock { autoTunnelMutex.withLock {
val settings = appDataRepository.settings.get() val settings = appDataRepository.settings.get()
appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = true)) appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = true))
if (autoTunnelService.isCompleted) { if (_autoTunnelService.value != null) return
_autoTunnelActive.update { true } withContext(ioDispatcher) {
return 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 { autoTunnelMutex.withLock {
val settings = appDataRepository.settings.get() val settings = appDataRepository.settings.get()
appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = false)) appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = false))
if (!autoTunnelService.isCompleted) return if (_autoTunnelService.value == null) return
runCatching { _autoTunnelService.value?.let { service ->
val service = autoTunnelService.await() service.stop()
service.stop() try {
_autoTunnelActive.update { false } context.unbindService(autoTunnelServiceConnection)
autoTunnelService = CompletableDeferred() } finally {
_tunnelService.value = null
} }
.onFailure { Timber.e(it) } }
withContext(mainDispatcher) { updateAutoTunnelTile() } withContext(mainDispatcher) { updateAutoTunnelTile() }
} }
} }
fun startTunnelForegroundService() { suspend fun startTunnelForegroundService() {
if (backgroundService.isCompleted) return if (_tunnelService.value != null) return
runCatching { withContext(ioDispatcher) {
backgroundService = CompletableDeferred() applicationScope.launch(ioDispatcher) {
startService( val intent = Intent(context, TunnelForegroundService::class.java)
TunnelForegroundService::class.java, context.startForegroundService(intent)
!WireGuardAutoTunnel.isForeground(), context.bindService(intent, tunnelServiceConnection, Context.BIND_AUTO_CREATE)
)
} }
.onFailure { Timber.e(it) } }
} }
suspend fun stopTunnelForegroundService() { fun stopTunnelForegroundService() {
if (!backgroundService.isCompleted) return _tunnelService.value?.let { service ->
runCatching { service.stop()
val service = backgroundService.await() try {
service.stop() context.unbindService(tunnelServiceConnection)
backgroundService = CompletableDeferred() } finally {
_tunnelService.value = null
} }
.onFailure { Timber.e(it) } }
} }
fun toggleAutoTunnel() { fun toggleAutoTunnel() {
applicationScope.launch(ioDispatcher) { applicationScope.launch(ioDispatcher) {
if (_autoTunnelActive.value) stopAutoTunnel() else startAutoTunnel() if (_autoTunnelService.value != null) stopAutoTunnel() else startAutoTunnel()
} }
} }
@@ -131,4 +139,12 @@ constructor(
fun updateTunnelTile() { fun updateTunnelTile() {
context.requestTunnelTileServiceStateUpdate() 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.app.Notification
import android.content.Intent import android.content.Intent
import android.os.Binder
import android.os.IBinder import android.os.IBinder
import androidx.core.app.ServiceCompat import androidx.core.app.ServiceCompat
import androidx.lifecycle.LifecycleService import androidx.lifecycle.LifecycleService
@@ -23,7 +24,6 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.distinctByKeys
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.NonCancellable
@@ -64,9 +64,12 @@ class TunnelForegroundService : LifecycleService() {
private val jobsMutex = Mutex() private val jobsMutex = Mutex()
class LocalBinder(val service: TunnelForegroundService) : Binder()
private val binder = LocalBinder(this)
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
serviceManager.backgroundService.complete(this)
ServiceCompat.startForeground( ServiceCompat.startForeground(
this@TunnelForegroundService, this@TunnelForegroundService,
NotificationManager.VPN_NOTIFICATION_ID, 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) super.onBind(intent)
return null return binder
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId) super.onStartCommand(intent, flags, startId)
serviceManager.backgroundService.complete(this)
ServiceCompat.startForeground( ServiceCompat.startForeground(
this@TunnelForegroundService, this@TunnelForegroundService,
NotificationManager.VPN_NOTIFICATION_ID, NotificationManager.VPN_NOTIFICATION_ID,
@@ -273,7 +275,7 @@ class TunnelForegroundService : LifecycleService() {
} }
override fun onDestroy() { override fun onDestroy() {
serviceManager.backgroundService = CompletableDeferred() serviceManager.handleTunnelServiceDestroy()
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
super.onDestroy() super.onDestroy()
} }
@@ -1,6 +1,7 @@
package com.zaneschepke.wireguardautotunnel.core.service.autotunnel package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
import android.content.Intent import android.content.Intent
import android.os.Binder
import android.os.IBinder import android.os.IBinder
import android.os.PowerManager import android.os.PowerManager
import androidx.core.app.ServiceCompat import androidx.core.app.ServiceCompat
@@ -28,7 +29,6 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider import javax.inject.Provider
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
@@ -68,21 +68,23 @@ class AutoTunnelService : LifecycleService() {
private var killSwitchJob: Job? = null private var killSwitchJob: Job? = null
class LocalBinder(val service: AutoTunnelService) : Binder()
private val binder = LocalBinder(this)
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
serviceManager.autoTunnelService.complete(this)
launchWatcherNotification() launchWatcherNotification()
} }
override fun onBind(intent: Intent): IBinder? { override fun onBind(intent: Intent): IBinder {
super.onBind(intent) super.onBind(intent)
return null return binder
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId) super.onStartCommand(intent, flags, startId)
Timber.d("onStartCommand executed with startId: $startId") Timber.d("onStartCommand executed with startId: $startId")
serviceManager.autoTunnelService.complete(this)
start() start()
return START_STICKY return START_STICKY
} }
@@ -105,7 +107,7 @@ class AutoTunnelService : LifecycleService() {
} }
override fun onDestroy() { override fun onDestroy() {
serviceManager.autoTunnelService = CompletableDeferred() serviceManager.handleAutoTunnelServiceDestroy()
restoreVpnKillSwitch() restoreVpnKillSwitch()
super.onDestroy() super.onDestroy()
} }
@@ -38,8 +38,8 @@ class AutoTunnelControlTile : TileService(), LifecycleOwner {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START) lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
Timber.d("Start listening called for auto tunnel tile") Timber.d("Start listening called for auto tunnel tile")
lifecycleScope.launch { lifecycleScope.launch {
serviceManager.autoTunnelActive.collect { serviceManager.autoTunnelService.collect {
if (it) return@collect setActive() if (it != null) return@collect setActive()
setInactive() setInactive()
} }
} }
@@ -56,7 +56,7 @@ class AutoTunnelControlTile : TileService(), LifecycleOwner {
super.onClick() super.onClick()
unlockAndRun { unlockAndRun {
lifecycleScope.launch { lifecycleScope.launch {
if (serviceManager.autoTunnelActive.value) { if (serviceManager.autoTunnelService.value != null) {
serviceManager.stopAutoTunnel() serviceManager.stopAutoTunnel()
setInactive() setInactive()
} else { } else {
@@ -1,6 +1,5 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.wireguard.android.backend.BackendException
import com.wireguard.android.backend.Tunnel import com.wireguard.android.backend.Tunnel
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
@@ -11,14 +10,11 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import kotlin.concurrent.thread import kotlin.concurrent.thread
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
@@ -31,6 +27,10 @@ abstract class BaseTunnel(
private val serviceManager: ServiceManager, private val serviceManager: ServiceManager,
) : TunnelProvider { ) : TunnelProvider {
private val _errorEvents =
MutableSharedFlow<Pair<TunnelConf, BackendError>>(replay = 0, extraBufferCapacity = 1)
override val errorEvents = _errorEvents.asSharedFlow()
private val activeTuns = MutableStateFlow<Map<TunnelConf, TunnelState>>(emptyMap()) private val activeTuns = MutableStateFlow<Map<TunnelConf, TunnelState>>(emptyMap())
private val tunThreads = ConcurrentHashMap<Int, Thread>() private val tunThreads = ConcurrentHashMap<Int, Thread>()
override val activeTunnels = activeTuns.asStateFlow() override val activeTunnels = activeTuns.asStateFlow()
@@ -45,37 +45,34 @@ abstract class BaseTunnel(
abstract fun stopBackend(tunnel: TunnelConf) abstract fun stopBackend(tunnel: TunnelConf)
override suspend fun clearError(tunnelConf: TunnelConf) =
updateTunnelStatus(tunnelConf, TunnelStatus.Down)
override fun hasVpnPermission(): Boolean { override fun hasVpnPermission(): Boolean {
return serviceManager.hasVpnPermission() return serviceManager.hasVpnPermission()
} }
protected suspend fun updateTunnelStatus( protected suspend fun updateTunnelStatus(
tunnelConf: TunnelConf, tunnelConf: TunnelConf,
state: TunnelStatus? = null, status: TunnelStatus? = null,
stats: TunnelStatistics? = null, stats: TunnelStatistics? = null,
) { ) {
tunStatusMutex.withLock { tunStatusMutex.withLock {
activeTuns.update { current -> activeTuns.update { currentTuns ->
val originalConf = current.getKeyById(tunnelConf.id) ?: tunnelConf val originalConf = currentTuns.getKeyById(tunnelConf.id) ?: tunnelConf
val existingState = current.getValueById(tunnelConf.id) ?: TunnelState() val existingState = currentTuns.getValueById(tunnelConf.id) ?: TunnelState()
val newState = state ?: existingState.status val newState = status ?: existingState.status
if (newState == TunnelStatus.Down) { if (newState == TunnelStatus.Down) {
Timber.d("Removing tunnel ${tunnelConf.id} from activeTunnels as state is DOWN") Timber.d("Removing tunnel ${tunnelConf.id} from activeTunnels as state is DOWN")
cleanUpTunThread(tunnelConf) cleanUpTunThread(tunnelConf)
current - originalConf currentTuns - originalConf
} else if (existingState.status == newState && stats == null) { } else if (existingState.status == newState && stats == null) {
Timber.d("Skipping redundant state update for ${tunnelConf.id}: $newState") Timber.d("Skipping redundant state update for ${tunnelConf.id}: $newState")
current currentTuns
} else { } else {
val updated = val updated =
existingState.copy( existingState.copy(
status = newState, status = newState,
statistics = stats ?: existingState.statistics, statistics = stats ?: existingState.statistics,
) )
current + (originalConf to updated) currentTuns + (originalConf to updated)
} }
} }
} }
@@ -117,23 +114,17 @@ abstract class BaseTunnel(
if (this@BaseTunnel is UserspaceTunnel) stopActiveTunnels() if (this@BaseTunnel is UserspaceTunnel) stopActiveTunnels()
tunMutex.withLock { tunMutex.withLock {
tunThreads[tunnelConf.id] = thread { tunThreads[tunnelConf.id] = thread {
runCatching { runBlocking {
runBlocking { try {
try { Timber.d("Starting tunnel ${tunnelConf.id}...")
Timber.d("Starting tunnel ${tunnelConf.id}...") startTunnelInner(tunnelConf)
startTunnelInner(tunnelConf) Timber.d("Started complete for tunnel ${tunnelConf.name}...")
Timber.d("Started complete for tunnel ${tunnelConf.name}...") } catch (e: InterruptedException) {
} catch (e: BackendError) { Timber.w(
Timber.e(e, "Failed to start tunnel ${tunnelConf.name} userspace") "Tunnel start has been interrupted as ${tunnelConf.name} failed to start"
updateTunnelStatus(tunnelConf, TunnelStatus.Error(e)) )
} catch (e: InterruptedException) {
Timber.w(
"Tunnel start has been interrupted as ${tunnelConf.name} failed to start"
)
}
}
} }
.onFailure { Timber.w("Tunnel start has been interrupted") } }
} }
} }
} }
@@ -147,11 +138,10 @@ abstract class BaseTunnel(
Timber.d("Started for tun ${tunnelConf.id}...") Timber.d("Started for tun ${tunnelConf.id}...")
saveTunnelActiveState(tunnelConf, true) saveTunnelActiveState(tunnelConf, true)
serviceManager.startTunnelForegroundService() serviceManager.startTunnelForegroundService()
} catch (e: BackendException) { } catch (e: BackendError) {
Timber.e(e, "Failed to start backend for ${tunnelConf.name}") Timber.e(e, "Failed to start backend for ${tunnelConf.name}")
val backendError = e.toBackendError() _errorEvents.emit(tunnelConf to e)
updateTunnelStatus(tunnelConf, TunnelStatus.Error(backendError)) updateTunnelStatus(tunnelConf, TunnelStatus.Down)
throw backendError
} }
} }
@@ -163,26 +153,27 @@ abstract class BaseTunnel(
override suspend fun stopTunnel(tunnelConf: TunnelConf?, reason: TunnelStatus.StopReason) { override suspend fun stopTunnel(tunnelConf: TunnelConf?, reason: TunnelStatus.StopReason) {
if (tunnelConf == null) return stopActiveTunnels() if (tunnelConf == null) return stopActiveTunnels()
tunMutex.withLock { tunMutex.withLock {
try { if (activeTuns.isStarting(tunnelConf.id))
if (activeTuns.isStarting(tunnelConf.id)) return handleStuckStartingTunnelShutdown(tunnelConf)
return handleStuckStartingTunnelShutdown(tunnelConf) updateTunnelStatus(tunnelConf, TunnelStatus.Stopping(reason))
updateTunnelStatus(tunnelConf, TunnelStatus.Stopping(reason)) stopTunnelInner(tunnelConf)
stopTunnelInner(tunnelConf)
} catch (e: BackendError) {
Timber.e(e, "Failed to stop tunnel ${tunnelConf.id}")
updateTunnelStatus(tunnelConf, TunnelStatus.Error(e))
}
} }
} }
private suspend fun stopTunnelInner(tunnelConf: TunnelConf) { private suspend fun stopTunnelInner(tunnelConf: TunnelConf) {
val tunnel = activeTuns.findTunnel(tunnelConf.id) ?: return try {
stopBackend(tunnel) val tunnel = activeTuns.findTunnel(tunnelConf.id) ?: return
saveTunnelActiveState(tunnelConf, false) stopBackend(tunnel)
removeActiveTunnel(tunnel) saveTunnelActiveState(tunnelConf, false)
removeActiveTunnel(tunnel)
} catch (e: BackendError) {
Timber.e(e, "Failed to stop tunnel ${tunnelConf.id}")
_errorEvents.emit(tunnelConf to e)
updateTunnelStatus(tunnelConf, TunnelStatus.Down)
}
} }
private suspend fun handleServiceStateOnChange() { private fun handleServiceStateOnChange() {
if (activeTuns.value.isEmpty() && bouncingTunnelIds.isEmpty()) if (activeTuns.value.isEmpty() && bouncingTunnelIds.isEmpty())
serviceManager.stopTunnelForegroundService() serviceManager.stopTunnelForegroundService()
} }
@@ -193,15 +184,15 @@ abstract class BaseTunnel(
tunThreads[tunnel.id]?.let { tunThreads[tunnel.id]?.let {
if (it.state != Thread.State.TERMINATED) { if (it.state != Thread.State.TERMINATED) {
it.interrupt() it.interrupt()
updateTunnelStatus(tunnel, TunnelStatus.Down)
} else { } else {
Timber.d("Thread already terminated") Timber.d("Thread already terminated")
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "Failed to stop tunnel thread for ${tunnel.name}") Timber.e(e, "Failed to stop tunnel thread for ${tunnel.name}")
} finally {
updateTunnelStatus(tunnel, TunnelStatus.Down)
} }
cleanUpTunThread(tunnel)
} }
private fun cleanUpTunThread(tunnel: TunnelConf) { private fun cleanUpTunThread(tunnel: TunnelConf) {
@@ -221,7 +212,7 @@ abstract class BaseTunnel(
bouncingTunnelIds[tunnelConf.id] = reason bouncingTunnelIds[tunnelConf.id] = reason
try { try {
stopTunnel(tunnelConf, reason) stopTunnel(tunnelConf, reason)
delay(300L) delay(BOUNCE_DELAY)
startTunnel(tunnelConf) startTunnel(tunnelConf)
} finally { } finally {
bouncingTunnelIds.remove(tunnelConf.id) bouncingTunnelIds.remove(tunnelConf.id)
@@ -235,4 +226,8 @@ abstract class BaseTunnel(
override suspend fun runningTunnelNames(): Set<String> = override suspend fun runningTunnelNames(): Set<String> =
activeTuns.value.keys.map { it.tunName }.toSet() activeTuns.value.keys.map { it.tunName }.toSet()
companion object {
const val BOUNCE_DELAY = 300L
}
} }
@@ -5,6 +5,7 @@ import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.di.Kernel import com.zaneschepke.wireguardautotunnel.di.Kernel
import com.zaneschepke.wireguardautotunnel.di.Userspace import com.zaneschepke.wireguardautotunnel.di.Userspace
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendError
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
@@ -15,6 +16,7 @@ import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
@@ -62,6 +64,9 @@ constructor(
initialValue = emptyMap(), initialValue = emptyMap(),
) )
override val errorEvents: SharedFlow<Pair<TunnelConf, BackendError>>
get() = tunnelProviderFlow.value.errorEvents
override val bouncingTunnelIds: ConcurrentHashMap<Int, TunnelStatus.StopReason> = override val bouncingTunnelIds: ConcurrentHashMap<Int, TunnelStatus.StopReason> =
tunnelProviderFlow.value.bouncingTunnelIds tunnelProviderFlow.value.bouncingTunnelIds
@@ -69,10 +74,6 @@ constructor(
return userspaceTunnel.hasVpnPermission() return userspaceTunnel.hasVpnPermission()
} }
override suspend fun clearError(tunnelConf: TunnelConf) {
tunnelProviderFlow.value.clearError(tunnelConf)
}
override suspend fun updateTunnelStatistics(tunnel: TunnelConf) { override suspend fun updateTunnelStatistics(tunnel: TunnelConf) {
tunnelProviderFlow.value.updateTunnelStatistics(tunnel) tunnelProviderFlow.value.updateTunnelStatistics(tunnel)
} }
@@ -1,11 +1,13 @@
package com.zaneschepke.wireguardautotunnel.core.tunnel package com.zaneschepke.wireguardautotunnel.core.tunnel
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendError
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus import com.zaneschepke.wireguardautotunnel.domain.enums.TunnelStatus
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
interface TunnelProvider { interface TunnelProvider {
@@ -46,11 +48,11 @@ interface TunnelProvider {
val activeTunnels: StateFlow<Map<TunnelConf, TunnelState>> val activeTunnels: StateFlow<Map<TunnelConf, TunnelState>>
val errorEvents: SharedFlow<Pair<TunnelConf, BackendError>>
val bouncingTunnelIds: ConcurrentHashMap<Int, TunnelStatus.StopReason> val bouncingTunnelIds: ConcurrentHashMap<Int, TunnelStatus.StopReason>
fun hasVpnPermission(): Boolean fun hasVpnPermission(): Boolean
suspend fun clearError(tunnelConf: TunnelConf)
suspend fun updateTunnelStatistics(tunnel: TunnelConf) suspend fun updateTunnelStatistics(tunnel: TunnelConf)
} }
@@ -50,8 +50,9 @@ constructor(
} catch (e: BackendException) { } catch (e: BackendException) {
Timber.e(e, "Failed to stop tunnel ${tunnel.id}") Timber.e(e, "Failed to stop tunnel ${tunnel.id}")
throw e.toBackendError() throw e.toBackendError()
} finally {
handlePreviouslyEnabledVpnKillSwitch()
} }
handlePreviouslyEnabledVpnKillSwitch()
} }
// stop vpn kill switch if we need to resolve DNS for peer endpoints // stop vpn kill switch if we need to resolve DNS for peer endpoints
@@ -69,7 +70,7 @@ constructor(
// restore vpn kill switch if needed // restore vpn kill switch if needed
private fun handlePreviouslyEnabledVpnKillSwitch() { private fun handlePreviouslyEnabledVpnKillSwitch() {
// let auto tunnel handle this if it is active // let auto tunnel handle this if it is active
if (!serviceManager.autoTunnelActive.value) { if (serviceManager.autoTunnelService.value == null) {
previousBackendState?.let { (state, lanEnabled) -> previousBackendState?.let { (state, lanEnabled) ->
Timber.d("Restoring kill switch configuration") Timber.d("Restoring kill switch configuration")
val lan = if (lanEnabled) TunnelConf.LAN_BYPASS_ALLOWED_IPS else emptyList() val lan = if (lanEnabled) TunnelConf.LAN_BYPASS_ALLOWED_IPS else emptyList()
@@ -57,7 +57,7 @@ constructor(
withContext(ioDispatcher) { withContext(ioDispatcher) {
Timber.i("Service worker started") Timber.i("Service worker started")
with(appDataRepository.settings.get()) { with(appDataRepository.settings.get()) {
if (isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) if (isAutoTunnelEnabled && serviceManager.autoTunnelService.value == null)
return@with serviceManager.startAutoTunnel() return@with serviceManager.startAutoTunnel()
if (tunnelManager.activeTunnels.value.isEmpty()) if (tunnelManager.activeTunnels.value.isEmpty())
tunnelManager.restorePreviousState() tunnelManager.restorePreviousState()
@@ -1,7 +1,6 @@
package com.zaneschepke.wireguardautotunnel.domain.enums package com.zaneschepke.wireguardautotunnel.domain.enums
sealed class TunnelStatus { sealed class TunnelStatus {
data class Error(val error: BackendError) : TunnelStatus()
data object Up : TunnelStatus() data object Up : TunnelStatus()
@@ -12,6 +12,7 @@ class AmneziaStatistics(private val statistics: Statistics) : TunnelStatistics()
rxBytes = stats.rxBytes, rxBytes = stats.rxBytes,
txBytes = stats.txBytes, txBytes = stats.txBytes,
latestHandshakeEpochMillis = stats.latestHandshakeEpochMillis, latestHandshakeEpochMillis = stats.latestHandshakeEpochMillis,
resolvedEndpoint = stats.resolvedEndpoint,
) )
} }
} }
@@ -8,6 +8,7 @@ abstract class TunnelStatistics {
val rxBytes: Long, val rxBytes: Long,
val txBytes: Long, val txBytes: Long,
val latestHandshakeEpochMillis: Long, val latestHandshakeEpochMillis: Long,
val resolvedEndpoint: String,
) )
abstract fun peerStats(peer: Key): PeerStats? abstract fun peerStats(peer: Key): PeerStats?
@@ -12,6 +12,7 @@ class WireGuardStatistics(private val statistics: Statistics) : TunnelStatistics
txBytes = peerStats.txBytes, txBytes = peerStats.txBytes,
rxBytes = peerStats.rxBytes, rxBytes = peerStats.rxBytes,
latestHandshakeEpochMillis = peerStats.latestHandshakeEpochMillis, latestHandshakeEpochMillis = peerStats.latestHandshakeEpochMillis,
resolvedEndpoint = peerStats.resolvedEndpoint,
) )
} }
} }
@@ -8,6 +8,9 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -21,49 +24,90 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.toThreeDecimalPlaceSt
@Composable @Composable
fun TunnelStatisticsRow(statistics: TunnelStatistics?, tunnelConf: TunnelConf) { fun TunnelStatisticsRow(statistics: TunnelStatistics?, tunnelConf: TunnelConf) {
val config = TunnelConf.configFromAmQuick(tunnelConf.wgQuick) val config = TunnelConf.configFromAmQuick(tunnelConf.wgQuick)
config.peers.forEach { peer -> Column(
Row( modifier = Modifier.fillMaxWidth().padding(start = 45.dp, bottom = 10.dp, end = 10.dp),
modifier = Modifier.fillMaxWidth().padding(end = 10.dp, bottom = 10.dp, start = 45.dp), verticalArrangement = Arrangement.spacedBy(10.dp, Alignment.CenterVertically),
verticalAlignment = Alignment.CenterVertically, horizontalAlignment = Alignment.Start,
horizontalArrangement = Arrangement.spacedBy(5.dp, Alignment.Start), ) {
) { config.peers.forEach { peer ->
val peerId = peer.publicKey.toBase64().subSequence(0, 3).toString() + "***" val peerId = remember { peer.publicKey.toBase64().subSequence(0, 3).toString() + "***" }
val peerRx = statistics?.peerStats(peer.publicKey)?.rxBytes ?: 0 val endpoint =
val peerTx = statistics?.peerStats(peer.publicKey)?.txBytes ?: 0 remember(statistics) { statistics?.peerStats(peer.publicKey)?.resolvedEndpoint }
val peerTxMB = NumberUtils.bytesToMB(peerTx).toThreeDecimalPlaceString() val peerRxMB by
val peerRxMB = NumberUtils.bytesToMB(peerRx).toThreeDecimalPlaceString() remember(statistics) {
val handshake = derivedStateOf {
statistics?.peerStats(peer.publicKey)?.latestHandshakeEpochMillis?.let { statistics
if (it == 0L) { ?.peerStats(peer.publicKey)
stringResource(R.string.never) ?.rxBytes
} else { ?.let { NumberUtils.bytesToMB(it) }
"${NumberUtils.getSecondsBetweenTimestampAndNow(it)} ${stringResource(R.string.sec)}" ?.toThreeDecimalPlaceString()
} }
} ?: stringResource(R.string.never) }
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { val peerTxMB by
remember(statistics) {
derivedStateOf {
statistics
?.peerStats(peer.publicKey)
?.txBytes
?.let { NumberUtils.bytesToMB(it) }
?.toThreeDecimalPlaceString()
}
}
val handshake by
remember(statistics) {
derivedStateOf {
statistics?.peerStats(peer.publicKey)?.latestHandshakeEpochMillis?.let {
if (it == 0L) {
null
} else {
"${NumberUtils.getSecondsBetweenTimestampAndNow(it)}"
}
}
}
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.Start),
) {
Text( Text(
stringResource(R.string.peer).lowercase() + ": $peerId", stringResource(R.string.peer).lowercase() + ": $peerId",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.outline, color = MaterialTheme.colorScheme.outline,
) )
Text( Text(
"tx: $peerTxMB MB", stringResource(R.string.handshake) +
": ${if(handshake == null) stringResource(R.string.never) else handshake + " " + stringResource(R.string.sec)}",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.outline, color = MaterialTheme.colorScheme.outline,
) )
} }
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.Start),
) {
Text( Text(
stringResource(R.string.handshake) + ": $handshake", "rx: ${peerRxMB ?: 0.00} MB",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.outline, color = MaterialTheme.colorScheme.outline,
) )
Text( Text(
"rx: $peerRxMB MB", "tx: ${peerTxMB ?: 0.00} MB",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.outline, color = MaterialTheme.colorScheme.outline,
) )
} }
if (endpoint != null) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.Start),
) {
Text(
"endpoint: $endpoint",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.outline,
)
}
}
} }
} }
} }
@@ -39,6 +39,8 @@ import java.time.Instant
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider import javax.inject.Provider
import kotlin.collections.component1
import kotlin.collections.component2
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
@@ -56,7 +58,7 @@ constructor(
@IoDispatcher private val ioDispatcher: CoroutineDispatcher, @IoDispatcher private val ioDispatcher: CoroutineDispatcher,
@MainDispatcher private val mainDispatcher: CoroutineDispatcher, @MainDispatcher private val mainDispatcher: CoroutineDispatcher,
@AppShell private val rootShell: Provider<RootShell>, @AppShell private val rootShell: Provider<RootShell>,
private val tunnelManager: TunnelManager, val tunnelManager: TunnelManager,
private val serviceManager: ServiceManager, private val serviceManager: ServiceManager,
private val logReader: LogReader, private val logReader: LogReader,
private val fileUtils: FileUtils, private val fileUtils: FileUtils,
@@ -86,7 +88,7 @@ constructor(
appDataRepository.tunnels.flow, appDataRepository.tunnels.flow,
appDataRepository.appState.flow, appDataRepository.appState.flow,
tunnelManager.activeTunnels, tunnelManager.activeTunnels,
serviceManager.autoTunnelActive, serviceManager.autoTunnelService.map { it != null },
networkMonitor.networkStatusFlow, networkMonitor.networkStatusFlow,
) { array -> ) { array ->
val settings = array[0] as AppSettings val settings = array[0] as AppSettings
@@ -206,7 +208,6 @@ constructor(
is AppEvent.ShowMessage -> handleShowMessage(event.message) is AppEvent.ShowMessage -> handleShowMessage(event.message)
is AppEvent.PopBackStack -> is AppEvent.PopBackStack ->
_appViewState.update { it.copy(popBackStack = event.pop) } _appViewState.update { it.copy(popBackStack = event.pop) }
is AppEvent.ClearTunnelError -> tunnelManager.clearError(event.tunnel)
AppEvent.ToggleRemoteControl -> handleToggleRemoteControl(state.appState) AppEvent.ToggleRemoteControl -> handleToggleRemoteControl(state.appState)
AppEvent.ClearSelectedTunnels -> clearSelectedTunnels() AppEvent.ClearSelectedTunnels -> clearSelectedTunnels()
is AppEvent.SetShowModal -> is AppEvent.SetShowModal ->
@@ -265,6 +266,9 @@ constructor(
} }
} }
private fun handleTunnelErrors() =
viewModelScope.launch { tunnelManager.errorEvents.collect { errorEvent -> } }
private suspend fun handleAppReadyCheck(tunnels: List<TunnelConf>) { private suspend fun handleAppReadyCheck(tunnels: List<TunnelConf>) {
if (tunnels.size == appDataRepository.tunnels.count()) { if (tunnels.size == appDataRepository.tunnels.count()) {
_appViewState.update { it.copy(isAppReady = true) } _appViewState.update { it.copy(isAppReady = true) }
@@ -106,8 +106,6 @@ sealed class AppEvent {
data class ShowMessage(val message: StringValue) : AppEvent() data class ShowMessage(val message: StringValue) : AppEvent()
data class ClearTunnelError(val tunnel: TunnelConf) : AppEvent()
data class PopBackStack(val pop: Boolean) : AppEvent() data class PopBackStack(val pop: Boolean) : AppEvent()
data class SetBottomSheet(val showSheet: AppViewState.BottomSheet) : AppEvent() data class SetBottomSheet(val showSheet: AppViewState.BottomSheet) : AppEvent()
+2 -2
View File
@@ -80,8 +80,8 @@ fun Project.computeVersionName(): String {
// Bump minor for pre-release // Bump minor for pre-release
val preReleaseVersion = Semver.of( val preReleaseVersion = Semver.of(
baseVersion.major, baseVersion.major,
baseVersion.minor + 1, baseVersion.minor,
0 0 + 1,
) )
"${preReleaseVersion}-beta+git.${getGitCommitHash()}" "${preReleaseVersion}-beta+git.${getGitCommitHash()}"
} }
+2 -2
View File
@@ -1,7 +1,7 @@
[versions] [versions]
accompanist = "0.37.2" accompanist = "0.37.2"
activityCompose = "1.10.1" activityCompose = "1.10.1"
amneziawgAndroid = "1.3.8" amneziawgAndroid = "1.3.10"
androidx-junit = "1.2.1" androidx-junit = "1.2.1"
appcompat = "1.7.0" appcompat = "1.7.0"
biometricKtx = "1.2.0-alpha05" biometricKtx = "1.2.0-alpha05"
@@ -23,7 +23,7 @@ roomVersion = "2.7.1"
semver4j = "3.1.0" semver4j = "3.1.0"
slf4jAndroid = "1.7.36" slf4jAndroid = "1.7.36"
timber = "5.0.1" timber = "5.0.1"
tunnel = "1.2.14" tunnel = "1.2.16"
androidGradlePlugin = "8.9.2" androidGradlePlugin = "8.9.2"
kotlin = "2.1.20" kotlin = "2.1.20"
ksp = "2.1.20-2.0.0" ksp = "2.1.20-2.0.0"