mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5e7379f94b | |||
| c71c4e5b29 | |||
| 7f0fea3766 | |||
| 53c19762ef | |||
| c98fa04f73 | |||
| aba0f7d4d3 | |||
| fa517b2124 | |||
| d7e2648393 |
@@ -4,7 +4,7 @@ WG Tunnel
|
|||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
An alternative Android client app for [WireGuard®](https://www.wireguard.com/)
|
An alternative Android client app for [WireGuard](https://www.wireguard.com/)
|
||||||
and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/)
|
and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/)
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
@@ -23,14 +23,14 @@ and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/)
|
|||||||
[](https://play.google.com/store/apps/details?id=com.zaneschepke.wireguardautotunnel)
|
[](https://play.google.com/store/apps/details?id=com.zaneschepke.wireguardautotunnel)
|
||||||
[](https://f-droid.org/packages/com.zaneschepke.wireguardautotunnel/)
|
[](https://f-droid.org/packages/com.zaneschepke.wireguardautotunnel/)
|
||||||
[](https://github.com/zaneschepke/fdroid)
|
[](https://github.com/zaneschepke/fdroid)
|
||||||
|
[](https://apps.obtainium.imranr.dev/redirect?r=obtainium://app/%7B%22id%22%3A%22com.zaneschepke.wireguardautotunnel%22%2C%22url%22%3A%22https%3A%2F%2Fgithub.com%2Fzaneschepke%2Fwgtunnel%22%2C%22author%22%3A%22zaneschepke%22%2C%22name%22%3A%22WG%20Tunnel%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Atrue%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Atrue%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22WG%20Tunnel%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22Zane%20Schepke%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22overrideSource%22%3Anull%7D)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
[](https://discord.gg/rbRRNh6H7V)
|
[<img src="https://img.shields.io/badge/Telegram-26A5E4.svg?style=for-the-badge&logo=Telegram&logoColor=white">](https://t.me/wgtunnel)
|
||||||
[](https://t.me/wgtunnel)
|
[<img src="https://img.shields.io/badge/Matrix-000000.svg?style=for-the-badge&logo=Matrix&logoColor=white">](https://matrix.to/#/#wg-tunnel:matrix.zaneschepke.com)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<details open="open">
|
<details open="open">
|
||||||
@@ -49,7 +49,7 @@ and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/)
|
|||||||
<div style="text-align: left;">
|
<div style="text-align: left;">
|
||||||
|
|
||||||
## About
|
## About
|
||||||
Inspired by the official [wireguard-android](https://github.com/WireGuard/wireguard-android) app, WG Tunnel was created to address features and support missing from the official app. This app combines support for both [WireGuard®](https://www.wireguard.com/)
|
Inspired by the official [wireguard-android](https://github.com/WireGuard/wireguard-android) app, WG Tunnel was created to address features and support missing from the official app. This app combines support for both [WireGuard](https://www.wireguard.com/)
|
||||||
and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/), with its primary feature of auto-tunneling (on-demand tunneling).
|
and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/), with its primary feature of auto-tunneling (on-demand tunneling).
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -61,14 +61,14 @@ and [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/), with its pr
|
|||||||
Thank you to the following:
|
Thank you to the following:
|
||||||
|
|
||||||
- All of the users that have helped contribute to the project with ideas, translations, feedback, bug reports, testing, and donations.
|
- All of the users that have helped contribute to the project with ideas, translations, feedback, bug reports, testing, and donations.
|
||||||
- [WireGuard®](https://www.wireguard.com/) - © Jason A. Donenfeld (https://github.com/WireGuard/wireguard-android)
|
- [WireGuard](https://www.wireguard.com/) - Jason A. Donenfeld (https://github.com/WireGuard/wireguard-android)
|
||||||
|
|
||||||
- [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) - Amnezia Team (https://github.com/amnezia-vpn/amneziawg-android)
|
- [AmneziaWG](https://docs.amnezia.org/documentation/amnezia-wg/) - Amnezia Team (https://github.com/amnezia-vpn/amneziawg-android)
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; flex-wrap: wrap; justify-content: center; gap: 10px;">
|
<div style="display: flex; flex-wrap: wrap; justify-content: left; gap: 10px;">
|
||||||
<img label="Main" src="fastlane/metadata/android/en-US/images/phoneScreenshots/main_screen.png" width="200" />
|
<img label="Main" src="fastlane/metadata/android/en-US/images/phoneScreenshots/main_screen.png" width="200" />
|
||||||
<img label="Settings" src="fastlane/metadata/android/en-US/images/phoneScreenshots/settings_screen.png" width="200" />
|
<img label="Settings" src="fastlane/metadata/android/en-US/images/phoneScreenshots/settings_screen.png" width="200" />
|
||||||
<img label="Auto" src="fastlane/metadata/android/en-US/images/phoneScreenshots/auto_screen.png" width="200" />
|
<img label="Auto" src="fastlane/metadata/android/en-US/images/phoneScreenshots/auto_screen.png" width="200" />
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel
|
package com.zaneschepke.wireguardautotunnel
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@@ -32,12 +34,14 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import androidx.navigation.toRoute
|
import androidx.navigation.toRoute
|
||||||
|
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||||
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
|
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
|
||||||
@@ -68,6 +72,7 @@ import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
|||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
@@ -83,6 +88,11 @@ class MainActivity : AppCompatActivity() {
|
|||||||
@Inject
|
@Inject
|
||||||
lateinit var shortcutManager: ShortcutManager
|
lateinit var shortcutManager: ShortcutManager
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var networkMonitor: NetworkMonitor
|
||||||
|
|
||||||
|
private var lastLocationPermissionState: Boolean? = null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
enableEdgeToEdge(
|
enableEdgeToEdge(
|
||||||
statusBarStyle = SystemBarStyle.Companion.auto(Color.TRANSPARENT, Color.TRANSPARENT),
|
statusBarStyle = SystemBarStyle.Companion.auto(Color.TRANSPARENT, Color.TRANSPARENT),
|
||||||
@@ -256,4 +266,22 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
checkPermissionAndNotify()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkPermissionAndNotify() {
|
||||||
|
val hasLocation = ContextCompat.checkSelfPermission(
|
||||||
|
this,
|
||||||
|
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
if (lastLocationPermissionState != hasLocation) {
|
||||||
|
Timber.d("Location permission changed to: $hasLocation")
|
||||||
|
if (hasLocation) {
|
||||||
|
networkMonitor.sendLocationPermissionsGrantedBroadcast()
|
||||||
|
}
|
||||||
|
lastLocationPermissionState = hasLocation
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-37
@@ -4,8 +4,6 @@ import android.app.Service
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService
|
import com.zaneschepke.wireguardautotunnel.core.service.autotunnel.AutoTunnelService
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.tile.AutoTunnelControlTile
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.tile.TunnelControlTile
|
|
||||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||||
@@ -20,7 +18,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@@ -36,8 +33,6 @@ class ServiceManager @Inject constructor(
|
|||||||
|
|
||||||
var autoTunnelService = CompletableDeferred<AutoTunnelService>()
|
var autoTunnelService = CompletableDeferred<AutoTunnelService>()
|
||||||
var backgroundService = CompletableDeferred<TunnelForegroundService>()
|
var backgroundService = CompletableDeferred<TunnelForegroundService>()
|
||||||
var autoTunnelTile = CompletableDeferred<AutoTunnelControlTile>()
|
|
||||||
var tunnelControlTile = CompletableDeferred<TunnelControlTile>()
|
|
||||||
|
|
||||||
private fun <T : Service> startService(cls: Class<T>, background: Boolean) {
|
private fun <T : Service> startService(cls: Class<T>, background: Boolean) {
|
||||||
runCatching {
|
runCatching {
|
||||||
@@ -61,19 +56,16 @@ class ServiceManager @Inject constructor(
|
|||||||
runCatching {
|
runCatching {
|
||||||
autoTunnelService = CompletableDeferred()
|
autoTunnelService = CompletableDeferred()
|
||||||
startService(AutoTunnelService::class.java, background)
|
startService(AutoTunnelService::class.java, background)
|
||||||
val service = withTimeoutOrNull(SERVICE_START_TIMEOUT) { autoTunnelService.await() }
|
|
||||||
?: throw IllegalStateException("AutoTunnelService start timed out")
|
|
||||||
service.start()
|
|
||||||
_autoTunnelActive.update { true }
|
_autoTunnelActive.update { true }
|
||||||
updateAutoTunnelTile()
|
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
Timber.e(it)
|
Timber.e(it)
|
||||||
_autoTunnelActive.update { false }
|
_autoTunnelActive.update { false }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
updateAutoTunnelTile()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startBackgroundService(tunnelConf: TunnelConf) {
|
fun startTunnelForegroundService(tunnelConf: TunnelConf) {
|
||||||
applicationScope.launch(ioDispatcher) {
|
applicationScope.launch(ioDispatcher) {
|
||||||
if (backgroundService.isCompleted) return@launch
|
if (backgroundService.isCompleted) return@launch
|
||||||
runCatching {
|
runCatching {
|
||||||
@@ -88,7 +80,19 @@ class ServiceManager @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopBackgroundService() {
|
fun updateTunnelForegroundServiceNotification(tunnelConf: TunnelConf) {
|
||||||
|
applicationScope.launch(ioDispatcher) {
|
||||||
|
if (!backgroundService.isCompleted) return@launch
|
||||||
|
runCatching {
|
||||||
|
val service = backgroundService.await()
|
||||||
|
service.start(tunnelConf)
|
||||||
|
}.onFailure {
|
||||||
|
Timber.e(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopTunnelForegroundService() {
|
||||||
applicationScope.launch(ioDispatcher) {
|
applicationScope.launch(ioDispatcher) {
|
||||||
if (!backgroundService.isCompleted) return@launch
|
if (!backgroundService.isCompleted) return@launch
|
||||||
runCatching {
|
runCatching {
|
||||||
@@ -107,34 +111,12 @@ class ServiceManager @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun updateAutoTunnelTile() {
|
fun updateAutoTunnelTile() {
|
||||||
withContext(ioDispatcher) {
|
context.requestAutoTunnelTileServiceUpdate()
|
||||||
runCatching {
|
|
||||||
val service = withTimeoutOrNull(SERVICE_START_TIMEOUT) { autoTunnelTile.await() }
|
|
||||||
?: run {
|
|
||||||
context.requestAutoTunnelTileServiceUpdate()
|
|
||||||
return@withContext
|
|
||||||
}
|
|
||||||
service.updateTileState()
|
|
||||||
}.onFailure {
|
|
||||||
Timber.e(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun updateTunnelTile() {
|
fun updateTunnelTile() {
|
||||||
withContext(ioDispatcher) {
|
context.requestTunnelTileServiceStateUpdate()
|
||||||
runCatching {
|
|
||||||
val service = withTimeoutOrNull(SERVICE_START_TIMEOUT) { tunnelControlTile.await() }
|
|
||||||
?: run {
|
|
||||||
context.requestTunnelTileServiceStateUpdate()
|
|
||||||
return@withContext
|
|
||||||
}
|
|
||||||
service.updateTileState()
|
|
||||||
}.onFailure {
|
|
||||||
Timber.e(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopAutoTunnel() {
|
fun stopAutoTunnel() {
|
||||||
|
|||||||
+1
-1
@@ -49,12 +49,12 @@ class TunnelForegroundService : LifecycleService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun stop() {
|
fun stop() {
|
||||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
|
||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
serviceManager.backgroundService = CompletableDeferred()
|
serviceManager.backgroundService = CompletableDeferred()
|
||||||
|
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+5
-3
@@ -94,9 +94,11 @@ class AutoTunnelService : LifecycleService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
Timber.d("onStartCommand executed with startId: $startId")
|
Timber.d("onStartCommand executed with startId: $startId")
|
||||||
serviceManager.autoTunnelService.complete(this)
|
serviceManager.autoTunnelService.complete(this)
|
||||||
return super.onStartCommand(intent, flags, startId)
|
start()
|
||||||
|
return START_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
fun start() {
|
fun start() {
|
||||||
@@ -178,8 +180,8 @@ class AutoTunnelService : LifecycleService() {
|
|||||||
combineSettings(),
|
combineSettings(),
|
||||||
appDataRepository.get().settings.flow
|
appDataRepository.get().settings.flow
|
||||||
.distinctUntilChanged { old, new -> old.isKernelEnabled == new.isKernelEnabled } // Only emit when isKernelEnabled changes
|
.distinctUntilChanged { old, new -> old.isKernelEnabled == new.isKernelEnabled } // Only emit when isKernelEnabled changes
|
||||||
.flatMapLatest { settings ->
|
.flatMapLatest {
|
||||||
networkMonitor.getNetworkStatusFlow(true, settings.isKernelEnabled)
|
networkMonitor.networkStatusFlow
|
||||||
.flowOn(ioDispatcher)
|
.flowOn(ioDispatcher)
|
||||||
.map { buildNetworkState(it) }
|
.map { buildNetworkState(it) }
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-19
@@ -4,57 +4,61 @@ import android.content.Intent
|
|||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.service.quicksettings.Tile
|
import android.service.quicksettings.Tile
|
||||||
import android.service.quicksettings.TileService
|
import android.service.quicksettings.TileService
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.LifecycleRegistry
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class AutoTunnelControlTile : TileService() {
|
class AutoTunnelControlTile : TileService(), LifecycleOwner {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var appDataRepository: AppDataRepository
|
lateinit var appDataRepository: AppDataRepository
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var serviceManager: ServiceManager
|
lateinit var serviceManager: ServiceManager
|
||||||
|
|
||||||
@Inject
|
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
|
||||||
@ApplicationScope
|
|
||||||
lateinit var applicationScope: CoroutineScope
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
serviceManager.autoTunnelTile.complete(this)
|
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
serviceManager.autoTunnelTile = CompletableDeferred()
|
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartListening() {
|
override fun onStartListening() {
|
||||||
super.onStartListening()
|
super.onStartListening()
|
||||||
serviceManager.autoTunnelTile.complete(this)
|
Timber.d("Start listening called for auto tunnel tile")
|
||||||
applicationScope.launch {
|
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
|
||||||
if (appDataRepository.tunnels.getAll().isEmpty()) return@launch setUnavailable()
|
lifecycleScope.launch {
|
||||||
updateTileState()
|
serviceManager.autoTunnelActive.collect {
|
||||||
|
if (it) setActive() else setInactive()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
lifecycleScope.launch {
|
||||||
|
appDataRepository.tunnels.flow.collect {
|
||||||
fun updateTileState() {
|
if (it.isEmpty()) {
|
||||||
serviceManager.autoTunnelActive.value.let {
|
setUnavailable()
|
||||||
if (it) setActive() else setInactive()
|
} else {
|
||||||
|
if (qsTile.state == Tile.STATE_ACTIVE) setInactive()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClick() {
|
override fun onClick() {
|
||||||
super.onClick()
|
super.onClick()
|
||||||
unlockAndRun {
|
unlockAndRun {
|
||||||
applicationScope.launch {
|
lifecycleScope.launch {
|
||||||
if (serviceManager.autoTunnelActive.value) {
|
if (serviceManager.autoTunnelActive.value) {
|
||||||
serviceManager.stopAutoTunnel()
|
serviceManager.stopAutoTunnel()
|
||||||
setInactive()
|
setInactive()
|
||||||
@@ -97,4 +101,7 @@ class AutoTunnelControlTile : TileService() {
|
|||||||
qsTile.updateTile()
|
qsTile.updateTile()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override val lifecycle: Lifecycle
|
||||||
|
get() = lifecycleRegistry
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-17
@@ -5,58 +5,63 @@ import android.os.Build
|
|||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.service.quicksettings.Tile
|
import android.service.quicksettings.Tile
|
||||||
import android.service.quicksettings.TileService
|
import android.service.quicksettings.TileService
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.LifecycleRegistry
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class TunnelControlTile : TileService() {
|
class TunnelControlTile : TileService(), LifecycleOwner {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var appDataRepository: AppDataRepository
|
lateinit var appDataRepository: AppDataRepository
|
||||||
|
|
||||||
@Inject
|
|
||||||
@ApplicationScope
|
|
||||||
lateinit var applicationScope: CoroutineScope
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var serviceManager: ServiceManager
|
lateinit var serviceManager: ServiceManager
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var tunnelManager: TunnelManager
|
lateinit var tunnelManager: TunnelManager
|
||||||
|
|
||||||
|
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
|
||||||
|
|
||||||
|
private var isCollecting = false
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
serviceManager.tunnelControlTile.complete(this)
|
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
serviceManager.tunnelControlTile = CompletableDeferred()
|
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartListening() {
|
override fun onStartListening() {
|
||||||
super.onStartListening()
|
super.onStartListening()
|
||||||
Timber.d("Start listening called")
|
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
|
||||||
serviceManager.tunnelControlTile.complete(this)
|
Timber.d("Start listening called for tunnel tile")
|
||||||
applicationScope.launch {
|
if (isCollecting) return
|
||||||
updateTileState()
|
isCollecting = true
|
||||||
|
lifecycleScope.launch {
|
||||||
|
tunnelManager.activeTunnels.collect {
|
||||||
|
updateTileState()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateTileState() = applicationScope.launch {
|
private fun updateTileState() = lifecycleScope.launch {
|
||||||
val tunnels = appDataRepository.tunnels.getAll()
|
val tunnels = appDataRepository.tunnels.getAll()
|
||||||
if (tunnels.isEmpty()) return@launch setUnavailable()
|
if (tunnels.isEmpty()) return@launch setUnavailable()
|
||||||
with(tunnelManager.activeTunnels.value) {
|
with(tunnelManager.activeTunnels.value) {
|
||||||
if (isNotEmpty()) if (size == 1) {
|
if (isNotEmpty()) if (size == 1) {
|
||||||
tunnels.firstOrNull { it.id == keys.first() }?.let { return@launch updateTile(it.tunName, true) }
|
tunnels.firstOrNull { it.id == keys.first().id }?.let { return@launch updateTile(it.tunName, true) }
|
||||||
} else {
|
} else {
|
||||||
return@launch updateTile(getString(R.string.multiple), true)
|
return@launch updateTile(getString(R.string.multiple), true)
|
||||||
}
|
}
|
||||||
@@ -69,7 +74,7 @@ class TunnelControlTile : TileService() {
|
|||||||
override fun onClick() {
|
override fun onClick() {
|
||||||
super.onClick()
|
super.onClick()
|
||||||
unlockAndRun {
|
unlockAndRun {
|
||||||
applicationScope.launch {
|
lifecycleScope.launch {
|
||||||
if (tunnelManager.activeTunnels.value.isNotEmpty()) return@launch tunnelManager.stopTunnel()
|
if (tunnelManager.activeTunnels.value.isNotEmpty()) return@launch tunnelManager.stopTunnel()
|
||||||
appDataRepository.getStartTunnelConfig()?.let {
|
appDataRepository.getStartTunnelConfig()?.let {
|
||||||
tunnelManager.startTunnel(it)
|
tunnelManager.startTunnel(it)
|
||||||
@@ -132,4 +137,6 @@ class TunnelControlTile : TileService() {
|
|||||||
Timber.e(it)
|
Timber.e(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
override val lifecycle: Lifecycle
|
||||||
|
get() = lifecycleRegistry
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
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.networkmonitor.NetworkMonitor
|
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||||
import com.zaneschepke.networkmonitor.NetworkStatus
|
import com.zaneschepke.networkmonitor.NetworkStatus
|
||||||
@@ -8,12 +9,10 @@ import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
|||||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
|
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider.Companion.CHECK_INTERVAL
|
|
||||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||||
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.BackendError
|
||||||
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
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||||
@@ -23,23 +22,23 @@ import com.zaneschepke.wireguardautotunnel.util.Constants
|
|||||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
|
import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.cancelWithMessage
|
import com.zaneschepke.wireguardautotunnel.util.extensions.cancelWithMessage
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.coroutineScope
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.isActive
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
|
||||||
|
|
||||||
open class BaseTunnel(
|
abstract class BaseTunnel(
|
||||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||||
@ApplicationScope private val applicationScope: CoroutineScope,
|
@ApplicationScope private val applicationScope: CoroutineScope,
|
||||||
private val networkMonitor: NetworkMonitor,
|
private val networkMonitor: NetworkMonitor,
|
||||||
@@ -48,167 +47,71 @@ open class BaseTunnel(
|
|||||||
private val notificationManager: NotificationManager,
|
private val notificationManager: NotificationManager,
|
||||||
) : TunnelProvider {
|
) : TunnelProvider {
|
||||||
|
|
||||||
internal val tunnels = MutableStateFlow<List<TunnelConf>>(emptyList())
|
companion object {
|
||||||
|
const val CHECK_INTERVAL = 1000L
|
||||||
|
}
|
||||||
|
|
||||||
private val _tunnelStates = MutableStateFlow<Map<Int, TunnelState>>(emptyMap())
|
protected val activeTuns = MutableStateFlow<Map<TunnelConf, TunnelState>>(emptyMap())
|
||||||
|
override val activeTunnels = activeTuns.asStateFlow()
|
||||||
|
|
||||||
private val tunnelJobs = ConcurrentHashMap<Int, Job>()
|
private val tunnelJobs = ConcurrentHashMap<Int, MutableList<Job>>()
|
||||||
|
|
||||||
private val isNetworkAvailable = AtomicBoolean(false)
|
protected val mutex = Mutex()
|
||||||
|
private val isNetworkConnected = MutableStateFlow(true)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
applicationScope.launch(ioDispatcher) {
|
applicationScope.launch(ioDispatcher) {
|
||||||
launch { startNetworkJob() }
|
launch { monitorNetworkStatus() }
|
||||||
launch { monitorTunnelConfigChanges() }
|
launch { monitorTunnelConfigChanges() }
|
||||||
tunnels.collect { tuns ->
|
|
||||||
val previousTunIds = tunnelJobs.keys.toSet()
|
|
||||||
val currentTunIds = tuns.map { it.id }.toSet()
|
|
||||||
val newTuns = tuns.filter { it.id !in previousTunIds }
|
|
||||||
val removedTunIds = previousTunIds - currentTunIds
|
|
||||||
|
|
||||||
newTuns.forEach { tun ->
|
|
||||||
Timber.d("Starting tunnel jobs for tun ${tun.name} (ID: ${tun.id})")
|
|
||||||
tunnelJobs[tun.id] = startTunnelJobs(tun)
|
|
||||||
}
|
|
||||||
|
|
||||||
removedTunIds.forEach { tunId ->
|
|
||||||
tunnelJobs[tunId]?.cancelWithMessage("Canceling tunnel jobs for tunnel ID: $tunId")
|
|
||||||
tunnelJobs.remove(tunId)
|
|
||||||
_tunnelStates.update { it - tunId }
|
|
||||||
serviceManager.updateTunnelTile()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startTunnelJobs(tunnel: TunnelConf) = applicationScope.launch(ioDispatcher) {
|
private fun startTunnelJobs(tunnel: TunnelConf): Job {
|
||||||
launch { startTunnelStatisticsJob(tunnel) }
|
return applicationScope.launch(ioDispatcher) {
|
||||||
if (tunnel.isPingEnabled) launch { startPingJob(tunnel) }
|
val jobs = mutableListOf<Job>()
|
||||||
}
|
jobs += launch { updateTunnelStatistics(tunnel) }
|
||||||
|
if (tunnel.isPingEnabled) jobs += launch { monitorTunnelPing(tunnel) }
|
||||||
private fun updateTunnelState(tunnelId: Int, newState: TunnelStatus) {
|
jobs.forEach { it.join() }
|
||||||
Timber.d("Updating tunnel state for ID $tunnelId to $newState")
|
|
||||||
_tunnelStates.update { current ->
|
|
||||||
val currentState = current[tunnelId]
|
|
||||||
val updatedState = currentState?.copy(state = newState) ?: TunnelState(state = newState)
|
|
||||||
val newMap = current + (tunnelId to updatedState)
|
|
||||||
Timber.d("New tunnel states: $newMap")
|
|
||||||
newMap
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun beforeStartTunnel(tunnelConf: TunnelConf) {
|
private suspend fun updateTunnelStatistics(tunnel: TunnelConf) {
|
||||||
tunnelConf.setStateChangeCallback { state ->
|
while (true) {
|
||||||
Timber.d("New tunnel state $state")
|
|
||||||
when (state) {
|
|
||||||
is Tunnel.State -> updateTunnelState(tunnelConf.id, state.asTunnelState())
|
|
||||||
is org.amnezia.awg.backend.Tunnel.State -> updateTunnelState(tunnelConf.id, state.asTunnelState())
|
|
||||||
}
|
|
||||||
applicationScope.launch(ioDispatcher) {
|
|
||||||
serviceManager.updateTunnelTile()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun startTunnel(tunnelConf: TunnelConf) {
|
|
||||||
applicationScope.launch(ioDispatcher) {
|
|
||||||
serviceManager.startBackgroundService(tunnelConf)
|
|
||||||
appDataRepository.tunnels.save(tunnelConf.copy(isActive = true))
|
|
||||||
addToActiveTunnels(tunnelConf)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun stopTunnel(tunnelConf: TunnelConf?) {
|
|
||||||
// Default empty implementation; subclasses override
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun bounceTunnel(tunnelConf: TunnelConf) {
|
|
||||||
stopTunnel(tunnelConf)
|
|
||||||
delay(1000)
|
|
||||||
startTunnel(tunnelConf)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
|
|
||||||
// Default empty implementation
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun runningTunnelNames(): Set<String> {
|
|
||||||
return emptySet()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics {
|
|
||||||
throw NotImplementedError("Get statistics not implemented in base class")
|
|
||||||
}
|
|
||||||
|
|
||||||
override val activeTunnels: StateFlow<Map<Int, TunnelState>>
|
|
||||||
get() = _tunnelStates.asStateFlow()
|
|
||||||
|
|
||||||
internal suspend fun onTunnelStop(tunnelConf: TunnelConf) {
|
|
||||||
appDataRepository.tunnels.save(tunnelConf.copy(isActive = false))
|
|
||||||
removeFromActiveTunnels(tunnelConf)
|
|
||||||
if (tunnels.value.isEmpty()) serviceManager.stopBackgroundService()
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun stopAllTunnels() {
|
|
||||||
tunnels.value.forEach {
|
|
||||||
stopTunnel(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addToActiveTunnels(conf: TunnelConf) {
|
|
||||||
tunnels.update {
|
|
||||||
it.toMutableList().apply {
|
|
||||||
add(conf)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun removeFromActiveTunnels(conf: TunnelConf) {
|
|
||||||
tunnels.update {
|
|
||||||
it.toMutableList().apply {
|
|
||||||
remove(conf)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun startNetworkJob() = coroutineScope {
|
|
||||||
networkMonitor.getNetworkStatusFlow(includeWifiSsid = false, useRootShell = false)
|
|
||||||
.flowOn(ioDispatcher).collect {
|
|
||||||
isNetworkAvailable.set(it !is NetworkStatus.Disconnected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun startPingJob(tunnel: TunnelConf) = coroutineScope {
|
|
||||||
while (isActive) {
|
|
||||||
runCatching {
|
runCatching {
|
||||||
if (isNetworkAvailable.get() && tunnel.isActive) {
|
val stats = getStatistics(tunnel)
|
||||||
|
updateTunnelState(tunnel, stats = stats)
|
||||||
|
}.onFailure { e ->
|
||||||
|
Timber.e(e, "Failed to update stats for ${tunnel.tunName}")
|
||||||
|
}
|
||||||
|
delay(CHECK_INTERVAL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun monitorTunnelPing(tunnel: TunnelConf) {
|
||||||
|
while (true) {
|
||||||
|
runCatching {
|
||||||
|
if (isNetworkConnected.value && tunnel.isActive) {
|
||||||
val pingSuccess = tunnel.isTunnelPingable(ioDispatcher)
|
val pingSuccess = tunnel.isTunnelPingable(ioDispatcher)
|
||||||
handlePingResult(tunnel, pingSuccess)
|
if (!pingSuccess) bounceTunnel(tunnel)
|
||||||
}
|
}
|
||||||
delay(tunnel.pingInterval ?: Constants.PING_INTERVAL)
|
}.onFailure { e ->
|
||||||
|
Timber.e(e, "Ping failed for ${tunnel.tunName}")
|
||||||
}
|
}
|
||||||
|
delay(tunnel.pingInterval ?: Constants.PING_INTERVAL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun handlePingResult(tunnel: TunnelConf, pingSuccess: Boolean) {
|
protected fun handleBackendThrowable(throwable: Throwable) {
|
||||||
if (!pingSuccess) {
|
val backendError = when (throwable) {
|
||||||
if (isNetworkAvailable.get()) {
|
is BackendException -> throwable.toBackendError()
|
||||||
Timber.i("Ping result: target was not reachable, bouncing the tunnel")
|
is org.amnezia.awg.backend.BackendException -> throwable.toBackendError()
|
||||||
bounceTunnel(tunnel)
|
else -> BackendError.Unknown
|
||||||
delay(tunnel.pingCooldown ?: Constants.PING_COOLDOWN)
|
|
||||||
} else {
|
|
||||||
Timber.i("Ping result: target was not reachable, but no network available")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Timber.i("Ping result: all ping targets were reached successfully")
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
internal fun handleBackendThrowable(backendError: BackendError) {
|
|
||||||
val message = when (backendError) {
|
val message = when (backendError) {
|
||||||
BackendError.Config -> StringValue.StringResource(R.string.start_failed_config)
|
BackendError.Config -> StringValue.StringResource(R.string.start_failed_config)
|
||||||
BackendError.DNS -> StringValue.StringResource(R.string.dns_error)
|
BackendError.DNS -> StringValue.StringResource(R.string.dns_error)
|
||||||
BackendError.Unauthorized -> StringValue.StringResource(R.string.unauthorized)
|
BackendError.Unauthorized -> StringValue.StringResource(R.string.unauthorized)
|
||||||
|
BackendError.Unknown -> StringValue.StringResource(R.string.unknown_error)
|
||||||
}
|
}
|
||||||
if (WireGuardAutoTunnel.isForeground()) {
|
if (WireGuardAutoTunnel.isForeground()) {
|
||||||
SnackbarController.showMessage(message)
|
SnackbarController.showMessage(message)
|
||||||
@@ -224,33 +127,133 @@ open class BaseTunnel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun monitorTunnelConfigChanges() = coroutineScope {
|
protected fun updateTunnelState(tunnelConf: TunnelConf, state: TunnelStatus? = null, stats: TunnelStatistics? = null) {
|
||||||
appDataRepository.tunnels.flow.collect { storageTuns ->
|
applicationScope.launch(ioDispatcher) {
|
||||||
storageTuns.forEach { storageTun ->
|
mutex.withLock {
|
||||||
val currentTun = tunnels.value.firstOrNull { it.id == storageTun.id }
|
activeTuns.update { current ->
|
||||||
if (currentTun != null) {
|
val originalConf = current.getKeyById(tunnelConf.id) ?: tunnelConf
|
||||||
if (!currentTun.isQuickConfigMatching(storageTun)) {
|
val existingState = current.getValueById(tunnelConf.id) ?: TunnelState()
|
||||||
Timber.d("Tunnel config changed for ID $storageTun, bouncing tunnel")
|
val newState = state ?: existingState.state
|
||||||
bounceTunnel(storageTun)
|
if (newState == TunnelStatus.DOWN) {
|
||||||
|
// Remove tunnel from activeTunnels when it goes DOWN
|
||||||
|
Timber.d("Removing tunnel ${tunnelConf.id} from activeTunnels as state is DOWN")
|
||||||
|
current - originalConf
|
||||||
|
} else if (existingState.state == newState && stats == null) {
|
||||||
|
Timber.d("Skipping redundant state update for ${tunnelConf.id}: $newState")
|
||||||
|
current
|
||||||
|
} else {
|
||||||
|
val updated = existingState.copy(
|
||||||
|
state = newState,
|
||||||
|
statistics = stats ?: existingState.statistics,
|
||||||
|
)
|
||||||
|
current + (originalConf to updated)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun startTunnelStatisticsJob(tunnel: TunnelConf) = coroutineScope {
|
protected suspend fun configureTunnel(tunnelConf: TunnelConf) {
|
||||||
while (this.isActive) {
|
// setup state change callback
|
||||||
runCatching {
|
tunnelConf.setStateChangeCallback { state ->
|
||||||
val stats = getStatistics(tunnel)
|
Timber.d("State change callback triggered for tunnel ${tunnelConf.id}: ${tunnelConf.tunName} with state $state at ${System.currentTimeMillis()}")
|
||||||
_tunnelStates.update { currentStates ->
|
when (state) {
|
||||||
val updatedState = currentStates[tunnel.id]?.copy(statistics = stats)
|
is Tunnel.State -> updateTunnelState(tunnelConf, state.asTunnelState())
|
||||||
?: TunnelState(statistics = stats)
|
is org.amnezia.awg.backend.Tunnel.State -> updateTunnelState(tunnelConf, state.asTunnelState())
|
||||||
currentStates + (tunnel.id to updatedState)
|
}
|
||||||
|
applicationScope.launch(ioDispatcher) { serviceManager.updateTunnelTile() }
|
||||||
|
}
|
||||||
|
|
||||||
|
activeTuns.update { current ->
|
||||||
|
current.filter { it.key != tunnelConf } + (tunnelConf to TunnelState())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected suspend fun onStartSuccess(tunnelConf: TunnelConf) {
|
||||||
|
val tunnelCopy = tunnelConf.copyWithCallback(isActive = true)
|
||||||
|
|
||||||
|
// start service
|
||||||
|
if (activeTuns.value.isEmpty()) {
|
||||||
|
serviceManager.startTunnelForegroundService(tunnelCopy)
|
||||||
|
} else {
|
||||||
|
serviceManager.updateTunnelForegroundServiceNotification(tunnelCopy)
|
||||||
|
}
|
||||||
|
// save active
|
||||||
|
appDataRepository.tunnels.save(tunnelCopy)
|
||||||
|
// start tunnel jobs
|
||||||
|
tunnelJobs[tunnelCopy.id] = mutableListOf(startTunnelJobs(tunnelConf))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun startTunnel(tunnelConf: TunnelConf) {
|
||||||
|
throw NotImplementedError("Must be implemented by subclass")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stopTunnel(tunnelConf: TunnelConf?) {
|
||||||
|
tunnelConf?.let {
|
||||||
|
applicationScope.launch(ioDispatcher) {
|
||||||
|
mutex.withLock {
|
||||||
|
removeActiveTunnel(tunnelConf)
|
||||||
|
tunnelJobs[tunnelConf.id]?.forEach { it.cancelWithMessage("Cancel tunnel job") }
|
||||||
|
tunnelJobs.remove(tunnelConf.id)
|
||||||
|
val lockedConf = it.copyWithCallback(isActive = false)
|
||||||
|
appDataRepository.tunnels.save(lockedConf)
|
||||||
|
|
||||||
|
// TODO improve to handle multiple tunnels
|
||||||
|
if (activeTuns.value.isEmpty()) {
|
||||||
|
Timber.d("No tunnels active, stopping background service")
|
||||||
|
serviceManager.stopTunnelForegroundService()
|
||||||
|
} else {
|
||||||
|
Timber.d("Other tunnels still active, updating service notification")
|
||||||
|
val nextActive = activeTuns.value.keys.firstOrNull()
|
||||||
|
if (nextActive != null) {
|
||||||
|
Timber.d("Next active tunnel: ${nextActive.id}")
|
||||||
|
serviceManager.updateTunnelForegroundServiceNotification(nextActive)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
delay(CHECK_INTERVAL)
|
|
||||||
}.onFailure { exception ->
|
|
||||||
Timber.e(exception, "Failed to update tunnel statistics for ${tunnel.tunName}")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun removeActiveTunnel(tunnelConf: TunnelConf) {
|
||||||
|
activeTuns.update { current ->
|
||||||
|
current.toMutableMap().apply { remove(tunnelConf) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun bounceTunnel(tunnelConf: TunnelConf) {
|
||||||
|
stopTunnel(tunnelConf)
|
||||||
|
delay(1000)
|
||||||
|
startTunnel(tunnelConf)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun monitorNetworkStatus() {
|
||||||
|
networkMonitor.networkStatusFlow
|
||||||
|
.flowOn(ioDispatcher)
|
||||||
|
.collectLatest { status ->
|
||||||
|
val isAvailable = status !is NetworkStatus.Disconnected
|
||||||
|
isNetworkConnected.value = isAvailable
|
||||||
|
Timber.d("Network status: $isAvailable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun monitorTunnelConfigChanges() {
|
||||||
|
appDataRepository.tunnels.flow.collectLatest { storedTunnels ->
|
||||||
|
mutex.withLock {
|
||||||
|
storedTunnels.forEach { stored ->
|
||||||
|
val current = activeTuns.value.keys.find { it.id == stored.id }
|
||||||
|
if (current != null && !current.isQuickConfigMatching(stored)) {
|
||||||
|
Timber.d("Config changed for ${stored.id}, bouncing")
|
||||||
|
bounceTunnel(stored)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics? {
|
||||||
|
throw NotImplementedError("Must be implemented by subclass")
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun runningTunnelNames(): Set<String> = activeTuns.value.keys.map { it.tunName }.toSet()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||||
|
|
||||||
|
fun Map<TunnelConf, TunnelState>.allDown(): Boolean {
|
||||||
|
return this.all { it.value.state.isDown() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Map<TunnelConf, TunnelState>.hasActive(): Boolean {
|
||||||
|
return this.any { it.value.state.isUp() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Map<TunnelConf, TunnelState>.getValueById(id: Int): TunnelState? {
|
||||||
|
val key = this.keys.find { it.id == id }
|
||||||
|
return key?.let { this@getValueById[it] }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Map<TunnelConf, TunnelState>.getKeyById(id: Int): TunnelConf? {
|
||||||
|
return this.keys.find { it.id == id }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Map<TunnelConf, TunnelState>.isUp(tunnelConf: TunnelConf): Boolean {
|
||||||
|
return this.getValueById(tunnelConf.id)?.state?.isUp() ?: false
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||||
|
|
||||||
import com.wireguard.android.backend.Backend
|
import com.wireguard.android.backend.Backend
|
||||||
import com.wireguard.android.backend.BackendException
|
|
||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel
|
||||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||||
@@ -10,13 +9,15 @@ import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
|||||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||||
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.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.WireGuardStatistics
|
import com.zaneschepke.wireguardautotunnel.domain.state.WireGuardStatistics
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError
|
import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -31,40 +32,60 @@ class KernelTunnel @Inject constructor(
|
|||||||
) : BaseTunnel(ioDispatcher, applicationScope, networkMonitor, appDataRepository, serviceManager, notificationManager) {
|
) : BaseTunnel(ioDispatcher, applicationScope, networkMonitor, appDataRepository, serviceManager, notificationManager) {
|
||||||
|
|
||||||
override fun startTunnel(tunnelConf: TunnelConf) {
|
override fun startTunnel(tunnelConf: TunnelConf) {
|
||||||
Timber.d("Starting tunnel ${tunnelConf.id} kernel")
|
Timber.i("Starting tunnel ${tunnelConf.id} kernel")
|
||||||
applicationScope.launch(ioDispatcher) {
|
applicationScope.launch(ioDispatcher) {
|
||||||
if (tunnels.value.any { it.id == tunnelConf.id }) return@launch Timber.w("Tunnel already running")
|
|
||||||
runCatching {
|
runCatching {
|
||||||
Timber.d("Setting backend state UP")
|
// tunnel already active
|
||||||
super.beforeStartTunnel(tunnelConf)
|
if (activeTuns.value.any { it.key.id == tunnelConf.id }) return@launch
|
||||||
backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toWgConfig())
|
|
||||||
Timber.d("Calling super.startTunnel")
|
mutex.withLock {
|
||||||
super.startTunnel(tunnelConf)
|
updateTunnelState(tunnelConf, TunnelStatus.STARTING)
|
||||||
}.onFailure {
|
|
||||||
Timber.e(it, "Failed to start tunnel ${tunnelConf.id} kernel")
|
// configure state callback and add to tunnels
|
||||||
onTunnelStop(tunnelConf)
|
configureTunnel(tunnelConf)
|
||||||
if (it is BackendException) {
|
|
||||||
handleBackendThrowable(it.toBackendError())
|
updateTunnelState(tunnelConf, backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toWgConfig()).asTunnelState())
|
||||||
} else {
|
|
||||||
Timber.e(it)
|
// run some actions after start success
|
||||||
|
onStartSuccess(tunnelConf)
|
||||||
}
|
}
|
||||||
|
}.onFailure { exception ->
|
||||||
|
Timber.e(exception, "Failed to start tunnel ${tunnelConf.id} kernel")
|
||||||
|
stopTunnel(tunnelConf)
|
||||||
|
handleBackendThrowable(exception)
|
||||||
|
}.onSuccess {
|
||||||
|
Timber.i("Tunnel ${tunnelConf.id} started successfully")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics {
|
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics? {
|
||||||
return WireGuardStatistics(backend.getStatistics(tunnelConf))
|
return try {
|
||||||
|
WireGuardStatistics(backend.getStatistics(tunnelConf))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun stopTunnel(tunnelConf: TunnelConf?) {
|
override fun stopTunnel(tunnelConf: TunnelConf?) {
|
||||||
applicationScope.launch(ioDispatcher) {
|
applicationScope.launch(ioDispatcher) {
|
||||||
runCatching {
|
runCatching {
|
||||||
tunnels.value.firstOrNull { it.id == tunnelConf?.id }?.let {
|
val originalTunnel = activeTuns.value.keys.find { it.id == tunnelConf?.id }
|
||||||
backend.setState(it, Tunnel.State.DOWN, it.toWgConfig())
|
if (originalTunnel != null) {
|
||||||
onTunnelStop(it)
|
Timber.i("Stopping tunnel ${originalTunnel.id} kernel")
|
||||||
} ?: stopAllTunnels()
|
mutex.withLock {
|
||||||
}.onFailure {
|
updateTunnelState(originalTunnel, backend.setState(originalTunnel, Tunnel.State.DOWN, originalTunnel.toWgConfig()).asTunnelState())
|
||||||
Timber.e(it)
|
super.stopTunnel(originalTunnel)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Timber.w("Tunnel not found in startedTunnels, stopping all tunnels")
|
||||||
|
activeTuns.value.keys.forEach { config ->
|
||||||
|
stopTunnel(config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.onFailure { e ->
|
||||||
|
Timber.e(e, "Failed to stop tunnel ${tunnelConf?.id}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ class TunnelManager @Inject constructor(
|
|||||||
return tunnelProviderFlow.value.runningTunnelNames()
|
return tunnelProviderFlow.value.runningTunnelNames()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics {
|
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics? {
|
||||||
return tunnelProviderFlow.value.getStatistics(tunnelConf)
|
return tunnelProviderFlow.value.getStatistics(tunnelConf)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ class TunnelManager @Inject constructor(
|
|||||||
val settings = appDataRepository.settings.get()
|
val settings = appDataRepository.settings.get()
|
||||||
if (settings.isRestoreOnBootEnabled) {
|
if (settings.isRestoreOnBootEnabled) {
|
||||||
val previouslyActiveTuns = appDataRepository.tunnels.getActive()
|
val previouslyActiveTuns = appDataRepository.tunnels.getActive()
|
||||||
val tunsToStart = previouslyActiveTuns.filterNot { tun -> activeTunnels.value.any { tun.id == it.key } }
|
val tunsToStart = previouslyActiveTuns.filterNot { tun -> activeTunnels.value.any { tun.id == it.key.id } }
|
||||||
if (settings.isKernelEnabled) {
|
if (settings.isKernelEnabled) {
|
||||||
return@launch tunsToStart.forEach {
|
return@launch tunsToStart.forEach {
|
||||||
startTunnel(it)
|
startTunnel(it)
|
||||||
|
|||||||
@@ -7,15 +7,11 @@ import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
|||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
interface TunnelProvider {
|
interface TunnelProvider {
|
||||||
companion object {
|
|
||||||
const val CHECK_INTERVAL = 1000L
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startTunnel(tunnelConf: TunnelConf)
|
fun startTunnel(tunnelConf: TunnelConf)
|
||||||
fun stopTunnel(tunnelConf: TunnelConf? = null)
|
fun stopTunnel(tunnelConf: TunnelConf? = null)
|
||||||
suspend fun bounceTunnel(tunnelConf: TunnelConf)
|
suspend fun bounceTunnel(tunnelConf: TunnelConf)
|
||||||
suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>)
|
suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>)
|
||||||
suspend fun runningTunnelNames(): Set<String>
|
suspend fun runningTunnelNames(): Set<String>
|
||||||
fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics
|
fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics?
|
||||||
val activeTunnels: StateFlow<Map<Int, TunnelState>>
|
val activeTunnels: StateFlow<Map<TunnelConf, TunnelState>>
|
||||||
}
|
}
|
||||||
|
|||||||
+59
-28
@@ -1,6 +1,5 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||||
|
|
||||||
import com.wireguard.android.backend.BackendException
|
|
||||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||||
@@ -8,14 +7,17 @@ import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
|||||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||||
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.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.AmneziaStatistics
|
import com.zaneschepke.wireguardautotunnel.domain.state.AmneziaStatistics
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asAmBackendState
|
import com.zaneschepke.wireguardautotunnel.util.extensions.asAmBackendState
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError
|
import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
import org.amnezia.awg.backend.Backend
|
import org.amnezia.awg.backend.Backend
|
||||||
import org.amnezia.awg.backend.Tunnel
|
import org.amnezia.awg.backend.Tunnel
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
@@ -32,27 +34,41 @@ class UserspaceTunnel @Inject constructor(
|
|||||||
) : BaseTunnel(ioDispatcher, applicationScope, networkMonitor, appDataRepository, serviceManager, notificationManager) {
|
) : BaseTunnel(ioDispatcher, applicationScope, networkMonitor, appDataRepository, serviceManager, notificationManager) {
|
||||||
|
|
||||||
override fun startTunnel(tunnelConf: TunnelConf) {
|
override fun startTunnel(tunnelConf: TunnelConf) {
|
||||||
|
Timber.i("Starting tunnel ${tunnelConf.id} userspace")
|
||||||
applicationScope.launch(ioDispatcher) {
|
applicationScope.launch(ioDispatcher) {
|
||||||
Timber.d("Starting tunnel ${tunnelConf.id} userspace")
|
|
||||||
if (tunnels.value.any { it.id == tunnelConf.id }) return@launch Timber.w("Tunnel already running")
|
|
||||||
if (tunnels.value.isNotEmpty()) {
|
|
||||||
Timber.d("Stopping all tunnels")
|
|
||||||
stopAllTunnels()
|
|
||||||
}
|
|
||||||
runCatching {
|
runCatching {
|
||||||
Timber.d("Setting backend state UP")
|
// tunnel already active
|
||||||
super.beforeStartTunnel(tunnelConf)
|
if (activeTuns.value.any { it.key.id == tunnelConf.id }) return@launch
|
||||||
backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toAmConfig())
|
|
||||||
Timber.d("Calling super.startTunnel")
|
// stop any active tunnels that aren't this one, userspace only
|
||||||
super.startTunnel(tunnelConf)
|
stopActiveTunnels()
|
||||||
}.onFailure {
|
|
||||||
Timber.e(it, "Failed to start tunnel ${tunnelConf.id} userspace")
|
mutex.withLock {
|
||||||
onTunnelStop(tunnelConf)
|
updateTunnelState(tunnelConf, TunnelStatus.STARTING)
|
||||||
if (it is BackendException) {
|
|
||||||
handleBackendThrowable(it.toBackendError())
|
// configure state callback and add to tunnels
|
||||||
} else {
|
configureTunnel(tunnelConf)
|
||||||
Timber.e(it)
|
|
||||||
|
updateTunnelState(tunnelConf, backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toAmConfig()).asTunnelState())
|
||||||
|
|
||||||
|
// run some actions after start success
|
||||||
|
onStartSuccess(tunnelConf)
|
||||||
}
|
}
|
||||||
|
}.onFailure { exception ->
|
||||||
|
Timber.e(exception, "Failed to start tunnel ${tunnelConf.id} userspace")
|
||||||
|
stopTunnel(tunnelConf)
|
||||||
|
handleBackendThrowable(exception)
|
||||||
|
}.onSuccess {
|
||||||
|
Timber.i("Tunnel ${tunnelConf.id} started successfully")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun stopActiveTunnels() {
|
||||||
|
activeTunnels.value.forEach { (config, state) ->
|
||||||
|
if (state.state.isUp()) {
|
||||||
|
stopTunnel(config)
|
||||||
|
delay(300)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,17 +76,27 @@ class UserspaceTunnel @Inject constructor(
|
|||||||
override fun stopTunnel(tunnelConf: TunnelConf?) {
|
override fun stopTunnel(tunnelConf: TunnelConf?) {
|
||||||
applicationScope.launch(ioDispatcher) {
|
applicationScope.launch(ioDispatcher) {
|
||||||
runCatching {
|
runCatching {
|
||||||
tunnels.value.firstOrNull { it.id == tunnelConf?.id }?.let {
|
val originalTunnel = activeTuns.value.keys.find { it.id == tunnelConf?.id }
|
||||||
backend.setState(it, Tunnel.State.DOWN, it.toAmConfig())
|
if (originalTunnel != null) {
|
||||||
onTunnelStop(it)
|
Timber.i("Stopping tunnel ${originalTunnel.id} userspace")
|
||||||
} ?: stopAllTunnels()
|
mutex.withLock {
|
||||||
}.onFailure {
|
updateTunnelState(originalTunnel, backend.setState(originalTunnel, Tunnel.State.DOWN, originalTunnel.toAmConfig()).asTunnelState())
|
||||||
Timber.e(it)
|
super.stopTunnel(originalTunnel)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Timber.w("Tunnel not found in startedTunnels, stopping all tunnels")
|
||||||
|
activeTuns.value.keys.forEach { config ->
|
||||||
|
stopTunnel(config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.onFailure { e ->
|
||||||
|
Timber.e(e, "Failed to stop tunnel ${tunnelConf?.id}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
|
override suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
|
||||||
|
Timber.d("Setting backend state: $backendState with allowedIps: $allowedIps")
|
||||||
backend.setBackendState(backendState.asAmBackendState(), allowedIps)
|
backend.setBackendState(backendState.asAmBackendState(), allowedIps)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +104,12 @@ class UserspaceTunnel @Inject constructor(
|
|||||||
return backend.runningTunnelNames
|
return backend.runningTunnelNames
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics {
|
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics? {
|
||||||
return AmneziaStatistics(backend.getStatistics(tunnelConf))
|
return try {
|
||||||
|
AmneziaStatistics(backend.getStatistics(tunnelConf))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "Failed to get stats for ${tunnelConf.tunName}")
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
|||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelProvider
|
||||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.UserspaceTunnel
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.UserspaceTunnel
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||||
|
import com.zaneschepke.wireguardautotunnel.domain.repository.AppSettingRepository
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
@@ -20,6 +21,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
|||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.amnezia.awg.backend.Backend
|
import org.amnezia.awg.backend.Backend
|
||||||
import org.amnezia.awg.backend.GoBackend
|
import org.amnezia.awg.backend.GoBackend
|
||||||
import org.amnezia.awg.backend.RootTunnelActionHandler
|
import org.amnezia.awg.backend.RootTunnelActionHandler
|
||||||
@@ -101,8 +103,8 @@ class TunnelModule {
|
|||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideNetworkMonitor(@ApplicationContext context: Context): NetworkMonitor {
|
fun provideNetworkMonitor(@ApplicationContext context: Context, settingsRepository: AppSettingRepository): NetworkMonitor {
|
||||||
return AndroidNetworkMonitor(context)
|
return AndroidNetworkMonitor(context) { runBlocking { settingsRepository.get().isWifiNameByShellEnabled } }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.domain.entity
|
package com.zaneschepke.wireguardautotunnel.domain.entity
|
||||||
|
|
||||||
|
import com.wireguard.android.backend.Tunnel
|
||||||
import com.wireguard.config.Config
|
import com.wireguard.config.Config
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isReachable
|
import com.zaneschepke.wireguardautotunnel.util.extensions.isReachable
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.toWgQuickString
|
import com.zaneschepke.wireguardautotunnel.util.extensions.toWgQuickString
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.serialization.Transient
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import org.amnezia.awg.backend.Tunnel
|
import kotlinx.coroutines.sync.withLock
|
||||||
import timber.log.Timber
|
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
|
||||||
data class TunnelConf(
|
data class TunnelConf(
|
||||||
val id: Int = 0,
|
val id: Int = 0,
|
||||||
@@ -29,10 +32,39 @@ data class TunnelConf(
|
|||||||
val isIpv4Preferred: Boolean = true,
|
val isIpv4Preferred: Boolean = true,
|
||||||
@Transient
|
@Transient
|
||||||
private var stateChangeCallback: ((Any) -> Unit)? = null,
|
private var stateChangeCallback: ((Any) -> Unit)? = null,
|
||||||
) : Tunnel, com.wireguard.android.backend.Tunnel {
|
) : Tunnel, org.amnezia.awg.backend.Tunnel {
|
||||||
|
|
||||||
fun setStateChangeCallback(callback: (Any) -> Unit) {
|
private val callbackMutex = Mutex()
|
||||||
stateChangeCallback = callback
|
|
||||||
|
suspend fun setStateChangeCallback(callback: (Any) -> Unit) {
|
||||||
|
callbackMutex.withLock {
|
||||||
|
stateChangeCallback = callback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun copyWithCallback(
|
||||||
|
id: Int = this.id,
|
||||||
|
tunName: String = this.tunName,
|
||||||
|
wgQuick: String = this.wgQuick,
|
||||||
|
tunnelNetworks: List<String> = this.tunnelNetworks,
|
||||||
|
isMobileDataTunnel: Boolean = this.isMobileDataTunnel,
|
||||||
|
isPrimaryTunnel: Boolean = this.isPrimaryTunnel,
|
||||||
|
amQuick: String = this.amQuick,
|
||||||
|
isActive: Boolean = this.isActive,
|
||||||
|
isPingEnabled: Boolean = this.isPingEnabled,
|
||||||
|
pingInterval: Long? = this.pingInterval,
|
||||||
|
pingCooldown: Long? = this.pingCooldown,
|
||||||
|
pingIp: String? = this.pingIp,
|
||||||
|
isEthernetTunnel: Boolean = this.isEthernetTunnel,
|
||||||
|
isIpv4Preferred: Boolean = this.isIpv4Preferred,
|
||||||
|
): TunnelConf {
|
||||||
|
return TunnelConf(
|
||||||
|
id, tunName, wgQuick, tunnelNetworks, isMobileDataTunnel, isPrimaryTunnel,
|
||||||
|
amQuick, isActive, isPingEnabled, pingInterval, pingCooldown, pingIp,
|
||||||
|
isEthernetTunnel, isIpv4Preferred,
|
||||||
|
).apply {
|
||||||
|
stateChangeCallback = this@TunnelConf.stateChangeCallback
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toAmConfig(): org.amnezia.awg.config.Config {
|
fun toAmConfig(): org.amnezia.awg.config.Config {
|
||||||
@@ -43,25 +75,40 @@ data class TunnelConf(
|
|||||||
return configFromWgQuick(wgQuick)
|
return configFromWgQuick(wgQuick)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getName(): String {
|
override fun getName(): String = tunName
|
||||||
return tunName
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isIpv4ResolutionPreferred(): Boolean {
|
override fun isIpv4ResolutionPreferred(): Boolean = isIpv4Preferred
|
||||||
return isIpv4Preferred
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStateChange(newState: com.wireguard.android.backend.Tunnel.State) {
|
override fun onStateChange(newState: org.amnezia.awg.backend.Tunnel.State) {
|
||||||
stateChangeCallback?.invoke(newState)
|
Timber.d("onStateChange called for tunnel $id: $tunName with state $newState")
|
||||||
|
runBlocking {
|
||||||
|
callbackMutex.withLock {
|
||||||
|
if (stateChangeCallback != null) {
|
||||||
|
Timber.d("Invoking stateChangeCallback for tunnel $id: $tunName with state $newState")
|
||||||
|
stateChangeCallback?.invoke(newState)
|
||||||
|
} else {
|
||||||
|
Timber.w("No stateChangeCallback set for tunnel $id: $tunName")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStateChange(newState: Tunnel.State) {
|
override fun onStateChange(newState: Tunnel.State) {
|
||||||
stateChangeCallback?.invoke(newState)
|
Timber.d("onStateChange called for tunnel $id: $tunName with state $newState")
|
||||||
|
runBlocking {
|
||||||
|
callbackMutex.withLock {
|
||||||
|
if (stateChangeCallback != null) {
|
||||||
|
Timber.d("Invoking stateChangeCallback for tunnel $id: $tunName with state $newState")
|
||||||
|
stateChangeCallback?.invoke(newState)
|
||||||
|
} else {
|
||||||
|
Timber.w("No stateChangeCallback set for tunnel $id: $tunName")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isQuickConfigMatching(updatedConf: TunnelConf): Boolean {
|
fun isQuickConfigMatching(updatedConf: TunnelConf): Boolean {
|
||||||
return updatedConf.wgQuick == wgQuick ||
|
return updatedConf.wgQuick == wgQuick || updatedConf.amQuick == amQuick
|
||||||
updatedConf.amQuick == amQuick
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isPingConfigMatching(updatedConf: TunnelConf): Boolean {
|
fun isPingConfigMatching(updatedConf: TunnelConf): Boolean {
|
||||||
@@ -78,7 +125,6 @@ data class TunnelConf(
|
|||||||
return@withContext InetAddress.getByName(pingIp)
|
return@withContext InetAddress.getByName(pingIp)
|
||||||
.isReachable(Constants.PING_TIMEOUT.toInt())
|
.isReachable(Constants.PING_TIMEOUT.toInt())
|
||||||
}
|
}
|
||||||
Timber.i("Pinging all peers")
|
|
||||||
config.peers.map { peer ->
|
config.peers.map { peer ->
|
||||||
peer.isReachable(isIpv4Preferred)
|
peer.isReachable(isIpv4Preferred)
|
||||||
}.all { true }
|
}.all { true }
|
||||||
@@ -88,14 +134,14 @@ data class TunnelConf(
|
|||||||
companion object {
|
companion object {
|
||||||
fun configFromWgQuick(wgQuick: String): Config {
|
fun configFromWgQuick(wgQuick: String): Config {
|
||||||
val inputStream: InputStream = wgQuick.byteInputStream()
|
val inputStream: InputStream = wgQuick.byteInputStream()
|
||||||
return inputStream.bufferedReader(Charsets.UTF_8).use {
|
return inputStream.bufferedReader(StandardCharsets.UTF_8).use {
|
||||||
Config.parse(it)
|
Config.parse(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun configFromAmQuick(amQuick: String): org.amnezia.awg.config.Config {
|
fun configFromAmQuick(amQuick: String): org.amnezia.awg.config.Config {
|
||||||
val inputStream: InputStream = amQuick.byteInputStream()
|
val inputStream: InputStream = amQuick.byteInputStream()
|
||||||
return inputStream.bufferedReader(Charsets.UTF_8).use {
|
return inputStream.bufferedReader(StandardCharsets.UTF_8).use {
|
||||||
org.amnezia.awg.config.Config.parse(it)
|
org.amnezia.awg.config.Config.parse(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,4 +4,5 @@ sealed class BackendError() {
|
|||||||
data object DNS : BackendError()
|
data object DNS : BackendError()
|
||||||
data object Unauthorized : BackendError()
|
data object Unauthorized : BackendError()
|
||||||
data object Config : BackendError()
|
data object Config : BackendError()
|
||||||
|
data object Unknown : BackendError()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package com.zaneschepke.wireguardautotunnel.domain.enums
|
|||||||
enum class TunnelStatus {
|
enum class TunnelStatus {
|
||||||
UP,
|
UP,
|
||||||
DOWN,
|
DOWN,
|
||||||
|
STARTING,
|
||||||
|
STOPPING,
|
||||||
;
|
;
|
||||||
|
|
||||||
fun isDown(): Boolean {
|
fun isDown(): Boolean {
|
||||||
|
|||||||
+15
-12
@@ -1,5 +1,8 @@
|
|||||||
package com.zaneschepke.wireguardautotunnel.domain.state
|
package com.zaneschepke.wireguardautotunnel.domain.state
|
||||||
|
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.allDown
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.hasActive
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.isUp
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.events.KillSwitchEvent
|
import com.zaneschepke.wireguardautotunnel.domain.events.KillSwitchEvent
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
|
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||||
@@ -7,7 +10,7 @@ import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
|
|||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isMatchingToWildcardList
|
import com.zaneschepke.wireguardautotunnel.util.extensions.isMatchingToWildcardList
|
||||||
|
|
||||||
data class AutoTunnelState(
|
data class AutoTunnelState(
|
||||||
val activeTunnels: Map<Int, TunnelState> = emptyMap(),
|
val activeTunnels: Map<TunnelConf, TunnelState> = emptyMap(),
|
||||||
val networkState: NetworkState = NetworkState(),
|
val networkState: NetworkState = NetworkState(),
|
||||||
val settings: AppSettings = AppSettings(),
|
val settings: AppSettings = AppSettings(),
|
||||||
val tunnels: List<TunnelConf> = emptyList(),
|
val tunnels: List<TunnelConf> = emptyList(),
|
||||||
@@ -20,12 +23,12 @@ data class AutoTunnelState(
|
|||||||
private fun isMobileTunnelDataChangeNeeded(): Boolean {
|
private fun isMobileTunnelDataChangeNeeded(): Boolean {
|
||||||
val preferredTunnel = preferredMobileDataTunnel()
|
val preferredTunnel = preferredMobileDataTunnel()
|
||||||
return preferredTunnel != null &&
|
return preferredTunnel != null &&
|
||||||
activeTunnels.isNotEmpty() && !activeTunnels.any { it.key == preferredTunnel.id }
|
activeTunnels.isNotEmpty() && !activeTunnels.isUp(preferredTunnel)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isEthernetTunnelChangeNeeded(): Boolean {
|
private fun isEthernetTunnelChangeNeeded(): Boolean {
|
||||||
val preferredTunnel = preferredEthernetTunnel()
|
val preferredTunnel = preferredEthernetTunnel()
|
||||||
return preferredTunnel != null && activeTunnels.isNotEmpty() && !activeTunnels.any { it.key == preferredTunnel.id }
|
return preferredTunnel != null && activeTunnels.isNotEmpty() && !activeTunnels.isUp(preferredTunnel)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun preferredMobileDataTunnel(): TunnelConf? {
|
private fun preferredMobileDataTunnel(): TunnelConf? {
|
||||||
@@ -45,11 +48,11 @@ data class AutoTunnelState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun startOnEthernet(): Boolean {
|
private fun startOnEthernet(): Boolean {
|
||||||
return networkState.isEthernetConnected && settings.isTunnelOnEthernetEnabled && activeTunnels.isEmpty()
|
return networkState.isEthernetConnected && settings.isTunnelOnEthernetEnabled && activeTunnels.allDown()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun stopOnEthernet(): Boolean {
|
private fun stopOnEthernet(): Boolean {
|
||||||
return networkState.isEthernetConnected && !settings.isTunnelOnEthernetEnabled && activeTunnels.isNotEmpty()
|
return networkState.isEthernetConnected && !settings.isTunnelOnEthernetEnabled && activeTunnels.hasActive()
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO test removed kill switch state check
|
// TODO test removed kill switch state check
|
||||||
@@ -67,11 +70,11 @@ data class AutoTunnelState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun stopOnMobileData(): Boolean {
|
private fun stopOnMobileData(): Boolean {
|
||||||
return isMobileDataActive() && !settings.isTunnelOnMobileDataEnabled && activeTunnels.isNotEmpty()
|
return isMobileDataActive() && !settings.isTunnelOnMobileDataEnabled && activeTunnels.hasActive()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startOnMobileData(): Boolean {
|
private fun startOnMobileData(): Boolean {
|
||||||
return isMobileDataActive() && settings.isTunnelOnMobileDataEnabled && activeTunnels.isEmpty()
|
return isMobileDataActive() && settings.isTunnelOnMobileDataEnabled && activeTunnels.allDown()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun changeOnMobileData(): Boolean {
|
private fun changeOnMobileData(): Boolean {
|
||||||
@@ -83,24 +86,24 @@ data class AutoTunnelState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun stopOnWifi(): Boolean {
|
private fun stopOnWifi(): Boolean {
|
||||||
return isWifiActive() && !settings.isTunnelOnWifiEnabled && activeTunnels.isNotEmpty()
|
return isWifiActive() && !settings.isTunnelOnWifiEnabled && activeTunnels.hasActive()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun stopOnTrustedWifi(): Boolean {
|
private fun stopOnTrustedWifi(): Boolean {
|
||||||
return isWifiActive() && settings.isTunnelOnWifiEnabled && activeTunnels.isNotEmpty() && isCurrentSSIDTrusted()
|
return isWifiActive() && settings.isTunnelOnWifiEnabled && activeTunnels.hasActive() && isCurrentSSIDTrusted()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startOnUntrustedWifi(): Boolean {
|
private fun startOnUntrustedWifi(): Boolean {
|
||||||
return isWifiActive() && settings.isTunnelOnWifiEnabled && activeTunnels.isEmpty() && !isCurrentSSIDTrusted()
|
return isWifiActive() && settings.isTunnelOnWifiEnabled && activeTunnels.allDown() && !isCurrentSSIDTrusted()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun changeOnUntrustedWifi(): Boolean {
|
private fun changeOnUntrustedWifi(): Boolean {
|
||||||
return isWifiActive() && settings.isTunnelOnWifiEnabled && activeTunnels.isNotEmpty() && !isCurrentSSIDTrusted() && !isWifiTunnelPreferred()
|
return isWifiActive() && settings.isTunnelOnWifiEnabled && activeTunnels.hasActive() && !isCurrentSSIDTrusted() && !isWifiTunnelPreferred()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isWifiTunnelPreferred(): Boolean {
|
private fun isWifiTunnelPreferred(): Boolean {
|
||||||
val preferred = preferredWifiTunnel()
|
val preferred = preferredWifiTunnel()
|
||||||
return activeTunnels.any { it.key == preferred?.id }
|
return preferred?.let { activeTunnels.isUp(it) } ?: true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun asAutoTunnelEvent(): AutoTunnelEvent {
|
fun asAutoTunnelEvent(): AutoTunnelEvent {
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.getValueById
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
||||||
@@ -227,7 +228,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
|
|||||||
key = { tunnel -> tunnel.id },
|
key = { tunnel -> tunnel.id },
|
||||||
) { tunnel ->
|
) { tunnel ->
|
||||||
val expanded = uiState.generalState.isTunnelStatsExpanded
|
val expanded = uiState.generalState.isTunnelStatsExpanded
|
||||||
val tunnelState = activeTunnels.getOrDefault(tunnel.id, TunnelState())
|
val tunnelState = activeTunnels.getValueById(tunnel.id) ?: TunnelState()
|
||||||
TunnelRowItem(
|
TunnelRowItem(
|
||||||
tunnelState.state.isUp(),
|
tunnelState.state.isUp(),
|
||||||
expanded,
|
expanded,
|
||||||
|
|||||||
+2
-1
@@ -32,6 +32,7 @@ import androidx.compose.ui.text.input.KeyboardType
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.core.tunnel.isUp
|
||||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.Route
|
import com.zaneschepke.wireguardautotunnel.ui.Route
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
|
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
|
||||||
@@ -195,7 +196,7 @@ fun OptionsScreen(tunnelConf: TunnelConf, appUiState: AppUiState, viewModel: Tun
|
|||||||
trailing = {
|
trailing = {
|
||||||
ScaledSwitch(
|
ScaledSwitch(
|
||||||
checked = tunnelConf.isPingEnabled,
|
checked = tunnelConf.isPingEnabled,
|
||||||
enabled = !appUiState.activeTunnels.containsKey(tunnelConf.id),
|
enabled = !appUiState.activeTunnels.isUp(tunnelConf),
|
||||||
onClick = { onPingToggle() },
|
onClick = { onPingToggle() },
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
|||||||
data class AppUiState(
|
data class AppUiState(
|
||||||
val appSettings: AppSettings = AppSettings(),
|
val appSettings: AppSettings = AppSettings(),
|
||||||
val tunnels: List<TunnelConf> = emptyList(),
|
val tunnels: List<TunnelConf> = emptyList(),
|
||||||
val activeTunnels: Map<Int, TunnelState> = emptyMap(),
|
val activeTunnels: Map<TunnelConf, TunnelState> = emptyMap(),
|
||||||
val generalState: GeneralState = GeneralState(),
|
val generalState: GeneralState = GeneralState(),
|
||||||
val autoTunnelActive: Boolean = false,
|
val autoTunnelActive: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -147,6 +147,8 @@ constructor(
|
|||||||
appDataRepository.appState.setLocalLogsEnabled(toggledOn)
|
appDataRepository.appState.setLocalLogsEnabled(toggledOn)
|
||||||
if (!toggledOn) {
|
if (!toggledOn) {
|
||||||
logReader.stop()
|
logReader.stop()
|
||||||
|
} else {
|
||||||
|
logReader.start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -275,13 +277,13 @@ constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun requestRoot(): Result<Unit> {
|
private suspend fun requestRoot(): Result<Unit> {
|
||||||
return withContext(ioDispatcher) {
|
return withContext(ioDispatcher) {
|
||||||
runCatching {
|
runCatching {
|
||||||
rootShell.get().start()
|
rootShell.get().start()
|
||||||
SnackbarController.Companion.showMessage(StringValue.StringResource(R.string.root_accepted))
|
SnackbarController.showMessage(StringValue.StringResource(R.string.root_accepted))
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
SnackbarController.Companion.showMessage(StringValue.StringResource(R.string.error_root_denied))
|
SnackbarController.showMessage(StringValue.StringResource(R.string.error_root_denied))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -351,7 +353,7 @@ constructor(
|
|||||||
runCatching {
|
runCatching {
|
||||||
val amConfig = tunnelConfig.toAmConfig()
|
val amConfig = tunnelConfig.toAmConfig()
|
||||||
val wgConfig = tunnelConfig.toWgConfig()
|
val wgConfig = tunnelConfig.toWgConfig()
|
||||||
val proxy = InterfaceProxy.Companion.from(amConfig.`interface`)
|
val proxy = InterfaceProxy.from(amConfig.`interface`)
|
||||||
if (proxy.includedApplications.isEmpty() && proxy.excludedApplications.isEmpty()) return@launch
|
if (proxy.includedApplications.isEmpty() && proxy.excludedApplications.isEmpty()) return@launch
|
||||||
if (proxy.includedApplications.retainAll(packages.toSet()) || proxy.excludedApplications.retainAll(packages.toSet())) {
|
if (proxy.includedApplications.retainAll(packages.toSet()) || proxy.excludedApplications.retainAll(packages.toSet())) {
|
||||||
updateTunnelConfig(tunnelConfig, amConfig = amConfig, wgConfig = wgConfig, `interface` = proxy)
|
updateTunnelConfig(tunnelConfig, amConfig = amConfig, wgConfig = wgConfig, `interface` = proxy)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
object Constants {
|
object Constants {
|
||||||
const val VERSION_NAME = "3.7.1"
|
const val VERSION_NAME = "3.7.2"
|
||||||
const val JVM_TARGET = "17"
|
const val JVM_TARGET = "17"
|
||||||
const val VERSION_CODE = 37100
|
const val VERSION_CODE = 37200
|
||||||
const val TARGET_SDK = 35
|
const val TARGET_SDK = 35
|
||||||
const val MIN_SDK = 26
|
const val MIN_SDK = 26
|
||||||
const val APP_ID = "com.zaneschepke.wireguardautotunnel"
|
const val APP_ID = "com.zaneschepke.wireguardautotunnel"
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
What's new:
|
||||||
|
- Auto tunnel regression fix
|
||||||
|
- Tile sync improvements
|
||||||
|
- Optimize wifi name querying
|
||||||
|
- Improve network monitoring permission checks
|
||||||
@@ -9,13 +9,13 @@ coreKtx = "1.15.0"
|
|||||||
datastorePreferences = "1.1.3"
|
datastorePreferences = "1.1.3"
|
||||||
desugar_jdk_libs = "2.1.5"
|
desugar_jdk_libs = "2.1.5"
|
||||||
espressoCore = "3.6.1"
|
espressoCore = "3.6.1"
|
||||||
hiltAndroid = "2.55"
|
hiltAndroid = "2.56"
|
||||||
hiltCompiler = "1.2.0"
|
hiltCompiler = "1.2.0"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
kotlinx-serialization-json = "1.8.0"
|
kotlinx-serialization-json = "1.8.0"
|
||||||
lifecycle-runtime-compose = "2.8.7"
|
lifecycle-runtime-compose = "2.8.7"
|
||||||
material3 = "1.3.1"
|
material3 = "1.3.1"
|
||||||
navigationCompose = "2.8.8"
|
navigationCompose = "2.8.9"
|
||||||
pinLockCompose = "1.0.4"
|
pinLockCompose = "1.0.4"
|
||||||
roomVersion = "2.6.1"
|
roomVersion = "2.6.1"
|
||||||
timber = "5.0.1"
|
timber = "5.0.1"
|
||||||
@@ -23,7 +23,7 @@ tunnel = "1.2.7"
|
|||||||
androidGradlePlugin = "8.8.0-alpha05"
|
androidGradlePlugin = "8.8.0-alpha05"
|
||||||
kotlin = "2.1.10"
|
kotlin = "2.1.10"
|
||||||
ksp = "2.1.10-1.0.31"
|
ksp = "2.1.10-1.0.31"
|
||||||
composeBom = "2025.02.00"
|
composeBom = "2025.03.00"
|
||||||
compose = "1.7.8"
|
compose = "1.7.8"
|
||||||
workRuntimeKtxVersion = "2.10.0"
|
workRuntimeKtxVersion = "2.10.0"
|
||||||
zxingAndroidEmbedded = "4.3.0"
|
zxingAndroidEmbedded = "4.3.0"
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"
|
||||||
>
|
>
|
||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
tools:targetApi="29" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
||||||
|
|||||||
+112
-38
@@ -1,74 +1,139 @@
|
|||||||
package com.zaneschepke.networkmonitor
|
package com.zaneschepke.networkmonitor
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.location.LocationManager
|
||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
import android.net.Network
|
import android.net.Network
|
||||||
import android.net.NetworkCapabilities
|
import android.net.NetworkCapabilities
|
||||||
import android.net.NetworkRequest
|
import android.net.NetworkRequest
|
||||||
import android.net.wifi.WifiManager
|
import android.net.wifi.WifiManager
|
||||||
|
import android.os.Build
|
||||||
import com.wireguard.android.util.RootShell
|
import com.wireguard.android.util.RootShell
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.callbackFlow
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
class AndroidNetworkMonitor(
|
class AndroidNetworkMonitor(
|
||||||
context: Context,
|
context: Context,
|
||||||
|
private val useRootShellCallback: suspend () -> Boolean,
|
||||||
) : NetworkMonitor {
|
) : NetworkMonitor {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val LOCATION_GRANTED = "LOCATION_PERMISSIONS_GRANTED"
|
||||||
|
const val LOCATION_SERVICES_FILTER = "android.location.PROVIDERS_CHANGED"
|
||||||
|
}
|
||||||
|
|
||||||
private val appContext = context.applicationContext
|
private val appContext = context.applicationContext
|
||||||
|
private val packageName = appContext.packageName
|
||||||
private val connectivityManager = appContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
private val connectivityManager = appContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||||
private val wifiManager = appContext.getSystemService(Context.WIFI_SERVICE) as WifiManager?
|
private val wifiManager = appContext.getSystemService(Context.WIFI_SERVICE) as WifiManager?
|
||||||
|
private val locationManager = appContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||||
private val rootShell = RootShell(context)
|
private val rootShell = RootShell(context)
|
||||||
|
|
||||||
private var includeWifiSsid = false
|
@get:Synchronized @set:Synchronized
|
||||||
private var useRootShell = false
|
var currentSsid: String? = null
|
||||||
|
|
||||||
|
@get:Synchronized @set:Synchronized
|
||||||
|
var wifiConnected = false
|
||||||
|
|
||||||
data class WifiState(val connected: Boolean = false, val ssid: String? = null)
|
data class WifiState(val connected: Boolean = false, val ssid: String? = null)
|
||||||
data class TransportState(val connected: Boolean = false)
|
data class TransportState(val connected: Boolean = false)
|
||||||
|
|
||||||
private val wifiFlow: Flow<WifiState> = callbackFlow {
|
private val wifiFlow: Flow<WifiState> = callbackFlow {
|
||||||
var currentSsid: String? = null
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
fun getWifiSsid(): String? {
|
fun getWifiSsid(): String? {
|
||||||
if (!includeWifiSsid || wifiManager == null) return null
|
return if (runBlocking { useRootShellCallback() }) {
|
||||||
return if (useRootShell) {
|
|
||||||
rootShell.getCurrentWifiName()
|
rootShell.getCurrentWifiName()
|
||||||
} else {
|
} else {
|
||||||
wifiManager.connectionInfo?.ssid?.trim('"')?.takeIf {
|
if (wifiManager == null) return null
|
||||||
it != "<unknown>" && it.isNotEmpty()
|
try {
|
||||||
|
wifiManager.connectionInfo?.ssid?.trim('"')?.takeIf { it.isNotEmpty() }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun handleUnknownWifi() {
|
||||||
|
val newSsid = getWifiSsid()
|
||||||
|
// Only update if new SSID is valid; preserve existing valid SSID otherwise
|
||||||
|
if (newSsid != null && newSsid != WifiManager.UNKNOWN_SSID) {
|
||||||
|
currentSsid = newSsid
|
||||||
|
trySend(WifiState(connected = wifiConnected, ssid = currentSsid))
|
||||||
|
} else if (currentSsid == null || currentSsid == WifiManager.UNKNOWN_SSID) {
|
||||||
|
currentSsid = newSsid
|
||||||
|
trySend(WifiState(connected = wifiConnected, ssid = currentSsid))
|
||||||
|
}
|
||||||
|
Timber.d("handleUnknownWifi: currentSsid=$currentSsid")
|
||||||
|
}
|
||||||
|
|
||||||
|
val locationPermissionReceiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
Timber.d("locationPermissionReceiver received intent with action: ${intent.action}")
|
||||||
|
if (intent.action == "$packageName.$LOCATION_GRANTED") {
|
||||||
|
Timber.d("Received update: Precise and all-the-time location permissions are enabled")
|
||||||
|
handleUnknownWifi()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val locationServicesReceiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
if (intent.action == LOCATION_SERVICES_FILTER) {
|
||||||
|
val isGpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
|
||||||
|
val isNetworkEnabled = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
|
||||||
|
val isLocationServicesEnabled = isGpsEnabled || isNetworkEnabled
|
||||||
|
Timber.d("Location Services state changed. Enabled: $isLocationServicesEnabled, GPS: $isGpsEnabled, Network: $isNetworkEnabled")
|
||||||
|
if (isLocationServicesEnabled) handleUnknownWifi()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use RECEIVER_NOT_EXPORTED for Android 14+ compatibility
|
||||||
|
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
Context.RECEIVER_EXPORTED
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
appContext.registerReceiver(
|
||||||
|
locationPermissionReceiver,
|
||||||
|
IntentFilter("$packageName.$LOCATION_GRANTED"),
|
||||||
|
flags,
|
||||||
|
)
|
||||||
|
|
||||||
|
appContext.registerReceiver(
|
||||||
|
locationServicesReceiver,
|
||||||
|
IntentFilter(LOCATION_SERVICES_FILTER),
|
||||||
|
flags,
|
||||||
|
)
|
||||||
|
|
||||||
val callback = object : ConnectivityManager.NetworkCallback() {
|
val callback = object : ConnectivityManager.NetworkCallback() {
|
||||||
override fun onAvailable(network: Network) {
|
override fun onAvailable(network: Network) {
|
||||||
Timber.d("Wi-Fi onAvailable: network=$network")
|
Timber.d("Wi-Fi onAvailable: network=$network")
|
||||||
val capabilities = connectivityManager.getNetworkCapabilities(network)
|
currentSsid = getWifiSsid()
|
||||||
val connected = capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true
|
wifiConnected = true
|
||||||
if (connected) {
|
trySend(WifiState(connected = true, ssid = currentSsid))
|
||||||
val ssid = getWifiSsid()
|
|
||||||
currentSsid = ssid
|
|
||||||
trySend(WifiState(connected = true, ssid = ssid))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLost(network: Network) {
|
override fun onLost(network: Network) {
|
||||||
Timber.d("Wi-Fi onLost: network=$network")
|
Timber.d("Wi-Fi onLost: network=$network")
|
||||||
currentSsid = null
|
currentSsid = null
|
||||||
|
wifiConnected = false
|
||||||
trySend(WifiState(connected = false, ssid = null))
|
trySend(WifiState(connected = false, ssid = null))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
|
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
|
||||||
val connected = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
Timber.d("Wi-Fi onCapabilitiesChanged: network=$network, networkCapabilities=$networkCapabilities")
|
||||||
val ssid = if (connected) getWifiSsid() else null
|
|
||||||
if (ssid != currentSsid) {
|
|
||||||
currentSsid = ssid
|
|
||||||
trySend(WifiState(connected = connected, ssid = ssid))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,7 +145,11 @@ class AndroidNetworkMonitor(
|
|||||||
connectivityManager.registerNetworkCallback(request, callback)
|
connectivityManager.registerNetworkCallback(request, callback)
|
||||||
trySend(WifiState())
|
trySend(WifiState())
|
||||||
|
|
||||||
awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
|
awaitClose {
|
||||||
|
connectivityManager.unregisterNetworkCallback(callback)
|
||||||
|
appContext.unregisterReceiver(locationPermissionReceiver)
|
||||||
|
appContext.unregisterReceiver(locationServicesReceiver)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val cellularFlow: Flow<TransportState> = callbackFlow {
|
private val cellularFlow: Flow<TransportState> = callbackFlow {
|
||||||
@@ -104,7 +173,9 @@ class AndroidNetworkMonitor(
|
|||||||
connectivityManager.registerNetworkCallback(request, callback)
|
connectivityManager.registerNetworkCallback(request, callback)
|
||||||
trySend(TransportState())
|
trySend(TransportState())
|
||||||
|
|
||||||
awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
|
awaitClose {
|
||||||
|
connectivityManager.unregisterNetworkCallback(callback)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val ethernetFlow: Flow<TransportState> = callbackFlow {
|
private val ethernetFlow: Flow<TransportState> = callbackFlow {
|
||||||
@@ -131,21 +202,24 @@ class AndroidNetworkMonitor(
|
|||||||
awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
|
awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getNetworkStatusFlow(includeWifiSsid: Boolean, useRootShell: Boolean): Flow<NetworkStatus> {
|
override val networkStatusFlow = combine(wifiFlow, cellularFlow, ethernetFlow) { wifi, cellular, ethernet ->
|
||||||
this.includeWifiSsid = includeWifiSsid
|
val hasAnyConnection = wifi.connected || cellular.connected || ethernet.connected
|
||||||
this.useRootShell = useRootShell
|
if (hasAnyConnection) {
|
||||||
return combine(wifiFlow, cellularFlow, ethernetFlow) { wifi, cellular, ethernet ->
|
NetworkStatus.Connected(
|
||||||
val hasAnyConnection = wifi.connected || cellular.connected || ethernet.connected
|
wifiSsid = wifi.ssid,
|
||||||
if (hasAnyConnection) {
|
wifiConnected = wifi.connected,
|
||||||
NetworkStatus.Connected(
|
cellularConnected = cellular.connected,
|
||||||
wifiSsid = wifi.ssid,
|
ethernetConnected = ethernet.connected,
|
||||||
wifiConnected = wifi.connected,
|
)
|
||||||
cellularConnected = cellular.connected,
|
} else {
|
||||||
ethernetConnected = ethernet.connected,
|
NetworkStatus.Disconnected
|
||||||
)
|
}.also { Timber.d("NetworkStatus: $it") }
|
||||||
} else {
|
}.distinctUntilChanged()
|
||||||
NetworkStatus.Disconnected
|
|
||||||
}.also { Timber.d("NetworkStatus: $it") }
|
override fun sendLocationPermissionsGrantedBroadcast() {
|
||||||
}.distinctUntilChanged()
|
val action = "$packageName.$LOCATION_GRANTED"
|
||||||
|
val intent = Intent(action)
|
||||||
|
Timber.d("Sending broadcast: $action")
|
||||||
|
appContext.sendBroadcast(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,5 +3,6 @@ package com.zaneschepke.networkmonitor
|
|||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface NetworkMonitor {
|
interface NetworkMonitor {
|
||||||
fun getNetworkStatusFlow(includeWifiSsid: Boolean, useRootShell: Boolean): Flow<NetworkStatus>
|
val networkStatusFlow: Flow<NetworkStatus>
|
||||||
|
fun sendLocationPermissionsGrantedBroadcast()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user