mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b0de6f7530 | |||
| 39fc9014d8 | |||
| 8860b45230 | |||
| e220b26d88 | |||
| 2302b473b4 | |||
| b01473073f | |||
| 3ae9a24ca4 | |||
| dc3f7fa736 | |||
| 93d6f8aa45 | |||
| 3ea4aea5cf | |||
| 68b41c8925 | |||
| 06de1f24c2 | |||
| a39feeeea6 | |||
| c1619ff012 | |||
| 2534b86005 | |||
| 15c550737c | |||
| e1e7e27bb5 | |||
| 8021c133a5 | |||
| 6009445a15 | |||
| f80af9dd5e | |||
| 3f912ed532 |
@@ -144,8 +144,8 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation(project(":logcatter"))
|
||||
implementation(project(":networkmonitor"))
|
||||
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
|
||||
+38
-1
@@ -2,4 +2,41 @@
|
||||
|
||||
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
|
||||
<fields>;
|
||||
}
|
||||
}
|
||||
|
||||
# Keep all classes in the org.xbill.DNS package and subpackages
|
||||
-keep class org.xbill.DNS.** { *; }
|
||||
-dontwarn org.xbill.DNS.**
|
||||
|
||||
# Preserve JNA classes if used (e.g., for IPHlpAPI on Windows)
|
||||
-keep class com.sun.jna.** { *; }
|
||||
-dontwarn com.sun.jna.**
|
||||
|
||||
# Keep DNS resolver configuration classes that might be loaded dynamically
|
||||
-keep class org.xbill.DNS.config.** { *; }
|
||||
-dontwarn org.xbill.DNS.config.**
|
||||
|
||||
-keep class org.xbill.DNS.** { *; }
|
||||
|
||||
# Prevent optimization issues with native or reflection-based calls
|
||||
-dontoptimize
|
||||
-dontshrink
|
||||
# Uncomment the above if errors persist, but use sparingly as they’re broad
|
||||
|
||||
# Suppress warnings about missing classes if not all features are used
|
||||
-dontwarn java.lang.management.**
|
||||
-dontwarn sun.nio.ch.**
|
||||
|
||||
-dontwarn com.google.api.client.http.GenericUrl
|
||||
-dontwarn com.google.api.client.http.HttpHeaders
|
||||
-dontwarn com.google.api.client.http.HttpRequest
|
||||
-dontwarn com.google.api.client.http.HttpRequestFactory
|
||||
-dontwarn com.google.api.client.http.HttpResponse
|
||||
-dontwarn com.google.api.client.http.HttpTransport
|
||||
-dontwarn com.google.api.client.http.javanet.NetHttpTransport$Builder
|
||||
-dontwarn com.google.api.client.http.javanet.NetHttpTransport
|
||||
-dontwarn javax.lang.model.element.Modifier
|
||||
-dontwarn org.joda.time.Instant
|
||||
-dontwarn org.slf4j.impl.StaticLoggerBinder
|
||||
-dontwarn org.slf4j.impl.StaticMDCBinder
|
||||
-dontwarn org.slf4j.impl.StaticMarkerBinder
|
||||
|
||||
Vendored
+38
-1
@@ -21,4 +21,41 @@
|
||||
#-renamesourcefileattribute SourceFile
|
||||
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
|
||||
<fields>;
|
||||
}
|
||||
}
|
||||
|
||||
# Keep all classes in the org.xbill.DNS package and subpackages
|
||||
-keep class org.xbill.DNS.** { *; }
|
||||
-dontwarn org.xbill.DNS.**
|
||||
|
||||
# Preserve JNA classes if used (e.g., for IPHlpAPI on Windows)
|
||||
-keep class com.sun.jna.** { *; }
|
||||
-dontwarn com.sun.jna.**
|
||||
|
||||
# Keep DNS resolver configuration classes that might be loaded dynamically
|
||||
-keep class org.xbill.DNS.config.** { *; }
|
||||
-dontwarn org.xbill.DNS.config.**
|
||||
|
||||
-keep class org.xbill.DNS.** { *; }
|
||||
|
||||
# Prevent optimization issues with native or reflection-based calls
|
||||
-dontoptimize
|
||||
-dontshrink
|
||||
# Uncomment the above if errors persist, but use sparingly as they’re broad
|
||||
|
||||
# Suppress warnings about missing classes if not all features are used
|
||||
-dontwarn java.lang.management.**
|
||||
-dontwarn sun.nio.ch.**
|
||||
|
||||
-dontwarn com.google.api.client.http.GenericUrl
|
||||
-dontwarn com.google.api.client.http.HttpHeaders
|
||||
-dontwarn com.google.api.client.http.HttpRequest
|
||||
-dontwarn com.google.api.client.http.HttpRequestFactory
|
||||
-dontwarn com.google.api.client.http.HttpResponse
|
||||
-dontwarn com.google.api.client.http.HttpTransport
|
||||
-dontwarn com.google.api.client.http.javanet.NetHttpTransport$Builder
|
||||
-dontwarn com.google.api.client.http.javanet.NetHttpTransport
|
||||
-dontwarn javax.lang.model.element.Modifier
|
||||
-dontwarn org.joda.time.Instant
|
||||
-dontwarn org.slf4j.impl.StaticLoggerBinder
|
||||
-dontwarn org.slf4j.impl.StaticMDCBinder
|
||||
-dontwarn org.slf4j.impl.StaticMarkerBinder
|
||||
|
||||
@@ -3,13 +3,8 @@
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<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_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<!--foreground service exempt android 14-->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"
|
||||
@@ -168,25 +163,17 @@
|
||||
</service>
|
||||
|
||||
<receiver
|
||||
android:name=".core.broadcast.BootReceiver"
|
||||
android:name=".core.broadcast.RestartReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.ACTION_BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
||||
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
|
||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name=".core.broadcast.AppUpdateReceiver"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name=".core.broadcast.KernelReceiver"
|
||||
android:exported="false"
|
||||
|
||||
@@ -40,7 +40,6 @@ import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.toRoute
|
||||
import com.zaneschepke.wireguardautotunnel.core.shortcut.ShortcutManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.worker.ServiceWorker
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppStateRepository
|
||||
import com.zaneschepke.wireguardautotunnel.ui.Route
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.BottomNavBar
|
||||
@@ -67,7 +66,6 @@ import com.zaneschepke.wireguardautotunnel.ui.screens.support.LogsScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.support.SupportScreen
|
||||
import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
|
||||
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
@@ -121,22 +119,13 @@ class MainActivity : AppCompatActivity() {
|
||||
viewModel.getEmitSplitTunnelApps(this@MainActivity)
|
||||
}
|
||||
|
||||
LaunchedEffect(appUiState.autoTunnelActive) {
|
||||
requestAutoTunnelTileServiceUpdate()
|
||||
}
|
||||
|
||||
with(appUiState.appSettings) {
|
||||
LaunchedEffect(isAutoTunnelEnabled) {
|
||||
this@MainActivity.requestAutoTunnelTileServiceUpdate()
|
||||
}
|
||||
LaunchedEffect(isShortcutsEnabled) {
|
||||
if (!isShortcutsEnabled) return@LaunchedEffect shortcutManager.removeShortcuts()
|
||||
shortcutManager.addShortcuts()
|
||||
}
|
||||
}
|
||||
|
||||
ServiceWorker.start(this)
|
||||
|
||||
CompositionLocalProvider(LocalNavController provides navController) {
|
||||
SnackbarControllerProvider { host ->
|
||||
WireguardAutoTunnelTheme(theme = appUiState.generalState.theme) {
|
||||
@@ -221,16 +210,17 @@ class MainActivity : AppCompatActivity() {
|
||||
composable<Route.Logs> {
|
||||
LogsScreen()
|
||||
}
|
||||
composable<Route.Config> {
|
||||
val args = it.toRoute<Route.Config>()
|
||||
composable<Route.Config> { backStack ->
|
||||
val args = backStack.toRoute<Route.Config>()
|
||||
val config =
|
||||
appUiState.tunnels.firstOrNull { it.id == args.id }
|
||||
ConfigScreen(config, viewModel)
|
||||
}
|
||||
composable<Route.TunnelOptions> {
|
||||
val args = it.toRoute<Route.TunnelOptions>()
|
||||
val config = appUiState.tunnels.first { it.id == args.id }
|
||||
OptionsScreen(config)
|
||||
composable<Route.TunnelOptions> { backStack ->
|
||||
val args = backStack.toRoute<Route.TunnelOptions>()
|
||||
appUiState.tunnels.firstOrNull { it.id == args.id }?.let { config ->
|
||||
OptionsScreen(config, appUiState)
|
||||
}
|
||||
}
|
||||
composable<Route.Lock> {
|
||||
PinLockScreen(viewModel)
|
||||
@@ -241,15 +231,17 @@ class MainActivity : AppCompatActivity() {
|
||||
composable<Route.KillSwitch> {
|
||||
KillSwitchScreen(appUiState, viewModel)
|
||||
}
|
||||
composable<Route.SplitTunnel> {
|
||||
val args = it.toRoute<Route.SplitTunnel>()
|
||||
val config = appUiState.tunnels.first { it.id == args.id }
|
||||
SplitTunnelScreen(config, viewModel)
|
||||
composable<Route.SplitTunnel> { backStack ->
|
||||
val args = backStack.toRoute<Route.SplitTunnel>()
|
||||
appUiState.tunnels.firstOrNull { it.id == args.id }?.let {
|
||||
SplitTunnelScreen(it, viewModel)
|
||||
}
|
||||
}
|
||||
composable<Route.TunnelAutoTunnel> {
|
||||
val args = it.toRoute<Route.TunnelOptions>()
|
||||
val config = appUiState.tunnels.first { it.id == args.id }
|
||||
TunnelAutoTunnelScreen(config, appUiState.appSettings)
|
||||
composable<Route.TunnelAutoTunnel> { backStack ->
|
||||
val args = backStack.toRoute<Route.TunnelOptions>()
|
||||
appUiState.tunnels.firstOrNull { it.id == args.id }?.let {
|
||||
TunnelAutoTunnelScreen(it, appUiState.appSettings)
|
||||
}
|
||||
}
|
||||
}
|
||||
BackHandler {
|
||||
|
||||
@@ -11,6 +11,7 @@ import androidx.work.Configuration
|
||||
import com.wireguard.android.backend.GoBackend
|
||||
import com.zaneschepke.logcatter.LogReader
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.worker.ServiceWorker
|
||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.di.MainDispatcher
|
||||
@@ -91,6 +92,8 @@ class WireGuardAutoTunnel : Application(), Configuration.Provider {
|
||||
}
|
||||
}
|
||||
|
||||
ServiceWorker.start(this)
|
||||
|
||||
applicationScope.launch {
|
||||
withContext(mainDispatcher) {
|
||||
if (appDataRepository.appState.isLocalLogsEnabled() && !isRunningOnTv()) logReader.initialize()
|
||||
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.broadcast
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
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.tunnel.TunnelManager
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class AppUpdateReceiver : BroadcastReceiver() {
|
||||
|
||||
@Inject
|
||||
@ApplicationScope
|
||||
lateinit var applicationScope: CoroutineScope
|
||||
|
||||
@Inject
|
||||
lateinit var appDataRepository: AppDataRepository
|
||||
|
||||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
|
||||
@Inject
|
||||
lateinit var tunnelManager: TunnelManager
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action != Intent.ACTION_MY_PACKAGE_REPLACED) return
|
||||
serviceManager.updateTunnelTile()
|
||||
serviceManager.updateAutoTunnelTile()
|
||||
applicationScope.launch {
|
||||
with(appDataRepository.settings.get()) {
|
||||
if (isRestoreOnBootEnabled) {
|
||||
// If auto tunnel is enabled, just start it and let auto tunnel start appropriate tun
|
||||
if (isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) return@launch serviceManager.startAutoTunnel(true)
|
||||
tunnelManager.restorePreviousState()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.broadcast
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
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.tunnel.TunnelManager
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class BootReceiver : BroadcastReceiver() {
|
||||
@Inject
|
||||
lateinit var appDataRepository: AppDataRepository
|
||||
|
||||
@Inject
|
||||
@ApplicationScope
|
||||
lateinit var applicationScope: CoroutineScope
|
||||
|
||||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
|
||||
@Inject
|
||||
lateinit var tunnelManager: TunnelManager
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (Intent.ACTION_BOOT_COMPLETED != intent.action) return
|
||||
serviceManager.updateTunnelTile()
|
||||
serviceManager.updateAutoTunnelTile()
|
||||
applicationScope.launch {
|
||||
with(appDataRepository.settings.get()) {
|
||||
if (isRestoreOnBootEnabled) {
|
||||
// If auto tunnel is enabled, just start it and let auto tunnel start appropriate tun
|
||||
if (isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) return@launch serviceManager.startAutoTunnel(true)
|
||||
tunnelManager.restorePreviousState()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -32,8 +32,8 @@ class KernelReceiver : BroadcastReceiver() {
|
||||
val action = intent.action ?: return
|
||||
applicationScope.launch {
|
||||
if (action == REFRESH_TUNNELS_ACTION) {
|
||||
tunnelManager.runningTunnelNames().forEach {
|
||||
val tunnel = tunnelRepository.findByTunnelName(it)
|
||||
tunnelManager.runningTunnelNames().forEach { name ->
|
||||
val tunnel = tunnelRepository.findByTunnelName(name)
|
||||
tunnel?.let {
|
||||
tunnelRepository.save(it.copy(isActive = true))
|
||||
}
|
||||
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.broadcast
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
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.tunnel.TunnelManager
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class RestartReceiver : BroadcastReceiver() {
|
||||
@Inject
|
||||
lateinit var appDataRepository: AppDataRepository
|
||||
|
||||
@Inject
|
||||
@ApplicationScope
|
||||
lateinit var applicationScope: CoroutineScope
|
||||
|
||||
@Inject
|
||||
lateinit var serviceManager: ServiceManager
|
||||
|
||||
@Inject
|
||||
lateinit var tunnelManager: TunnelManager
|
||||
|
||||
@Inject
|
||||
@IoDispatcher
|
||||
lateinit var ioDispatcher: CoroutineDispatcher
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val action = intent.action ?: return
|
||||
if (action != Intent.ACTION_BOOT_COMPLETED &&
|
||||
action != Intent.ACTION_MY_PACKAGE_REPLACED &&
|
||||
action != "com.htc.intent.action.QUICKBOOT_POWERON"
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
Timber.d("RestartReceiver triggered with action: ${intent.action}")
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
serviceManager.updateTunnelTile()
|
||||
serviceManager.updateAutoTunnelTile()
|
||||
val settings = appDataRepository.settings.get()
|
||||
if (settings.isRestoreOnBootEnabled) {
|
||||
if (settings.isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) {
|
||||
Timber.d("Starting auto-tunnel on boot/update")
|
||||
serviceManager.startAutoTunnel(true)
|
||||
} else {
|
||||
Timber.d("Restoring previous tunnel state")
|
||||
tunnelManager.restorePreviousState()
|
||||
}
|
||||
} else {
|
||||
Timber.d("Restore on boot disabled, skipping")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-161
@@ -1,161 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.network
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkRequest
|
||||
import android.net.wifi.SupplicantState
|
||||
import android.net.wifi.WifiManager
|
||||
import android.os.Build
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.ConnectivityState
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import javax.inject.Inject
|
||||
|
||||
class InternetConnectivityMonitor
|
||||
@Inject
|
||||
constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) : NetworkMonitor {
|
||||
|
||||
private val connectivityManager =
|
||||
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
|
||||
@get:Synchronized @set:Synchronized
|
||||
private var wifiCapabilities: NetworkCapabilities? = null
|
||||
|
||||
@get:Synchronized @set:Synchronized
|
||||
private var wifiNetworkChanged: Boolean = false
|
||||
|
||||
override val didWifiChangeSinceLastCapabilitiesQuery: Boolean
|
||||
get() = wifiNetworkChanged
|
||||
|
||||
override val status = callbackFlow {
|
||||
|
||||
var wifiState: Boolean = false
|
||||
var ethernetState: Boolean = false
|
||||
var cellularState: Boolean = false
|
||||
|
||||
fun emitState() {
|
||||
trySend(ConnectivityState(wifiState, ethernetState, cellularState))
|
||||
}
|
||||
|
||||
val currentNetwork = connectivityManager.activeNetwork
|
||||
if (currentNetwork == null) {
|
||||
emitState()
|
||||
}
|
||||
|
||||
fun updateCapabilityState(up: Boolean, network: Network) {
|
||||
with(connectivityManager.getNetworkCapabilities(network)) {
|
||||
when {
|
||||
this == null -> return
|
||||
hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> wifiState = up
|
||||
hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ->
|
||||
cellularState = up
|
||||
|
||||
hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) ->
|
||||
ethernetState = up
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onWifiChange(network: Network, callback: () -> Unit) {
|
||||
if (connectivityManager.getNetworkCapabilities(network)?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true) {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
fun onAvailable(network: Network) {
|
||||
onWifiChange(network) {
|
||||
wifiNetworkChanged = true
|
||||
}
|
||||
}
|
||||
|
||||
fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
|
||||
onWifiChange(network) {
|
||||
wifiCapabilities = networkCapabilities
|
||||
}
|
||||
updateCapabilityState(true, network)
|
||||
emitState()
|
||||
}
|
||||
|
||||
val networkStatusCallback =
|
||||
when (Build.VERSION.SDK_INT) {
|
||||
in Build.VERSION_CODES.S..Int.MAX_VALUE -> {
|
||||
object :
|
||||
ConnectivityManager.NetworkCallback(
|
||||
FLAG_INCLUDE_LOCATION_INFO,
|
||||
) {
|
||||
override fun onAvailable(network: Network) {
|
||||
onAvailable(network)
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
updateCapabilityState(false, network)
|
||||
emitState()
|
||||
}
|
||||
|
||||
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
|
||||
onCapabilitiesChanged(network, networkCapabilities)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
onAvailable(network)
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
updateCapabilityState(false, network)
|
||||
emitState()
|
||||
}
|
||||
|
||||
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
|
||||
onCapabilitiesChanged(network, networkCapabilities)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val request =
|
||||
NetworkRequest.Builder()
|
||||
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
.addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
|
||||
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||
.build()
|
||||
connectivityManager.registerNetworkCallback(request, networkStatusCallback)
|
||||
|
||||
awaitClose { connectivityManager.unregisterNetworkCallback(networkStatusCallback) }
|
||||
}.flowOn(ioDispatcher)
|
||||
|
||||
override fun getWifiCapabilities(): NetworkCapabilities? {
|
||||
wifiNetworkChanged = false
|
||||
return wifiCapabilities
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun getNetworkName(networkCapabilities: NetworkCapabilities, context: Context): String? {
|
||||
var ssid = networkCapabilities.getWifiName()
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) {
|
||||
val wifiManager =
|
||||
context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
val info = wifiManager.connectionInfo
|
||||
if (info.supplicantState === SupplicantState.COMPLETED) {
|
||||
ssid = info.ssid
|
||||
}
|
||||
}
|
||||
return ssid?.trim('"')
|
||||
}
|
||||
}
|
||||
}
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.network
|
||||
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.wifi.WifiInfo
|
||||
import android.os.Build
|
||||
|
||||
fun NetworkCapabilities.getWifiName(): String? {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val info: WifiInfo
|
||||
if (transportInfo is WifiInfo) {
|
||||
info = transportInfo as WifiInfo
|
||||
return info.ssid
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.network
|
||||
|
||||
import android.net.NetworkCapabilities
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.ConnectivityState
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface NetworkMonitor {
|
||||
val status: Flow<ConnectivityState>
|
||||
|
||||
// util to help limit location queries
|
||||
val didWifiChangeSinceLastCapabilitiesQuery: Boolean
|
||||
fun getWifiCapabilities(): NetworkCapabilities?
|
||||
}
|
||||
+94
-53
@@ -3,29 +3,35 @@ package com.zaneschepke.wireguardautotunnel.core.service
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||
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.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.requestAutoTunnelTileServiceUpdate
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
|
||||
import jakarta.inject.Inject
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import timber.log.Timber
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class ServiceManager
|
||||
@Inject constructor(private val context: Context, private val ioDispatcher: CoroutineDispatcher, private val appDataRepository: AppDataRepository) {
|
||||
class ServiceManager @Inject constructor(
|
||||
private val context: Context,
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
@ApplicationScope private val applicationScope: CoroutineScope,
|
||||
private val appDataRepository: AppDataRepository,
|
||||
) {
|
||||
|
||||
private val _autoTunnelActive = MutableStateFlow(false)
|
||||
|
||||
val autoTunnelActive = _autoTunnelActive.asStateFlow()
|
||||
|
||||
var autoTunnelService = CompletableDeferred<AutoTunnelService>()
|
||||
@@ -44,76 +50,111 @@ class ServiceManager
|
||||
}.onFailure { Timber.e(it) }
|
||||
}
|
||||
|
||||
suspend fun startAutoTunnel(background: Boolean) {
|
||||
val settings = appDataRepository.settings.get()
|
||||
appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = true))
|
||||
if (autoTunnelService.isCompleted) return _autoTunnelActive.update { true }
|
||||
runCatching {
|
||||
startService(AutoTunnelService::class.java, background)
|
||||
autoTunnelService.await()
|
||||
autoTunnelService.getCompleted().start()
|
||||
_autoTunnelActive.update { true }
|
||||
updateAutoTunnelTile()
|
||||
}.onFailure {
|
||||
Timber.e(it)
|
||||
fun startAutoTunnel(background: Boolean) {
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
val settings = appDataRepository.settings.get()
|
||||
appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = true))
|
||||
if (autoTunnelService.isCompleted) {
|
||||
_autoTunnelActive.update { true }
|
||||
return@launch
|
||||
}
|
||||
runCatching {
|
||||
autoTunnelService = CompletableDeferred()
|
||||
startService(AutoTunnelService::class.java, background)
|
||||
val service = withTimeoutOrNull(SERVICE_START_TIMEOUT) { autoTunnelService.await() }
|
||||
?: throw IllegalStateException("AutoTunnelService start timed out")
|
||||
service.start()
|
||||
_autoTunnelActive.update { true }
|
||||
updateAutoTunnelTile()
|
||||
}.onFailure {
|
||||
Timber.e(it)
|
||||
_autoTunnelActive.update { false }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun startBackgroundService(tunnelConf: TunnelConf) {
|
||||
if (backgroundService.isCompleted) return
|
||||
runCatching {
|
||||
startService(TunnelForegroundService::class.java, true)
|
||||
backgroundService.await()
|
||||
backgroundService.getCompleted().start(tunnelConf)
|
||||
}.onFailure {
|
||||
Timber.e(it)
|
||||
fun startBackgroundService(tunnelConf: TunnelConf) {
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
if (backgroundService.isCompleted) return@launch
|
||||
runCatching {
|
||||
backgroundService = CompletableDeferred()
|
||||
startService(TunnelForegroundService::class.java, true)
|
||||
val service = withTimeoutOrNull(SERVICE_START_TIMEOUT) { backgroundService.await() }
|
||||
?: throw IllegalStateException("Background service start timed out")
|
||||
service.start(tunnelConf)
|
||||
}.onFailure {
|
||||
Timber.e(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun stopBackgroundService() {
|
||||
if (!backgroundService.isCompleted) return
|
||||
runCatching {
|
||||
backgroundService.getCompleted().stop()
|
||||
}.onFailure {
|
||||
Timber.e(it)
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
if (!backgroundService.isCompleted) return@launch
|
||||
runCatching {
|
||||
val service = backgroundService.await()
|
||||
service.stop()
|
||||
backgroundService = CompletableDeferred()
|
||||
}.onFailure {
|
||||
Timber.e(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun toggleAutoTunnel(background: Boolean) {
|
||||
fun toggleAutoTunnel(background: Boolean) {
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
if (_autoTunnelActive.value) stopAutoTunnel() else startAutoTunnel(background)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateAutoTunnelTile() {
|
||||
withContext(ioDispatcher) {
|
||||
if (_autoTunnelActive.value) return@withContext stopAutoTunnel()
|
||||
startAutoTunnel(background)
|
||||
runCatching {
|
||||
val service = withTimeoutOrNull(SERVICE_START_TIMEOUT) { autoTunnelTile.await() }
|
||||
?: run {
|
||||
context.requestAutoTunnelTileServiceUpdate()
|
||||
return@withContext
|
||||
}
|
||||
service.updateTileState()
|
||||
}.onFailure {
|
||||
Timber.e(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateAutoTunnelTile() {
|
||||
if (autoTunnelTile.isCompleted) {
|
||||
autoTunnelTile.getCompleted().updateTileState()
|
||||
} else {
|
||||
context.requestAutoTunnelTileServiceUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateTunnelTile() {
|
||||
if (tunnelControlTile.isCompleted) {
|
||||
tunnelControlTile.getCompleted().updateTileState()
|
||||
} else {
|
||||
context.requestTunnelTileServiceStateUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun stopAutoTunnel() {
|
||||
suspend fun updateTunnelTile() {
|
||||
withContext(ioDispatcher) {
|
||||
runCatching {
|
||||
val service = withTimeoutOrNull(SERVICE_START_TIMEOUT) { tunnelControlTile.await() }
|
||||
?: run {
|
||||
context.requestTunnelTileServiceStateUpdate()
|
||||
return@withContext
|
||||
}
|
||||
service.updateTileState()
|
||||
}.onFailure {
|
||||
Timber.e(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun stopAutoTunnel() {
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
val settings = appDataRepository.settings.get()
|
||||
appDataRepository.settings.save(settings.copy(isAutoTunnelEnabled = false))
|
||||
if (!autoTunnelService.isCompleted) return@withContext
|
||||
if (!autoTunnelService.isCompleted) return@launch
|
||||
runCatching {
|
||||
autoTunnelService.getCompleted().stop()
|
||||
val service = autoTunnelService.await()
|
||||
service.stop()
|
||||
_autoTunnelActive.update { false }
|
||||
autoTunnelService = CompletableDeferred()
|
||||
updateAutoTunnelTile()
|
||||
}.onFailure {
|
||||
Timber.e(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val SERVICE_START_TIMEOUT = 5_000L
|
||||
}
|
||||
}
|
||||
|
||||
+42
-43
@@ -1,44 +1,42 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.service.autotunnel
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.NetworkCapabilities
|
||||
import android.os.IBinder
|
||||
import android.os.PowerManager
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.lifecycle.LifecycleService
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.wireguard.android.util.RootShell
|
||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||
import com.zaneschepke.networkmonitor.NetworkStatus
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.di.AppShell
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.di.MainImmediateDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.AutoTunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.NetworkState
|
||||
import com.zaneschepke.wireguardautotunnel.core.network.InternetConnectivityMonitor
|
||||
import com.zaneschepke.wireguardautotunnel.core.network.NetworkMonitor
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.ConnectivityState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.TunnelManager
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.di.MainImmediateDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.NotificationAction
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
|
||||
import com.zaneschepke.wireguardautotunnel.domain.events.KillSwitchEvent
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.AutoTunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.NetworkState
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.Tunnels
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.getCurrentWifiName
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -49,10 +47,6 @@ import javax.inject.Provider
|
||||
@AndroidEntryPoint
|
||||
class AutoTunnelService : LifecycleService() {
|
||||
|
||||
@Inject
|
||||
@AppShell
|
||||
lateinit var rootShell: Provider<RootShell>
|
||||
|
||||
@Inject
|
||||
lateinit var networkMonitor: NetworkMonitor
|
||||
|
||||
@@ -161,49 +155,54 @@ class AutoTunnelService : LifecycleService() {
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun buildNetworkState(connectivityState: ConnectivityState): NetworkState {
|
||||
private fun buildNetworkState(networkStatus: NetworkStatus): NetworkState {
|
||||
return with(autoTunnelStateFlow.value.networkState) {
|
||||
val wifiName = when {
|
||||
connectivityState.wifiAvailable &&
|
||||
(wifiName == null || wifiName == Constants.UNREADABLE_SSID || networkMonitor.didWifiChangeSinceLastCapabilitiesQuery) -> {
|
||||
networkMonitor.getWifiCapabilities()?.let { getWifiName(it) } ?: wifiName
|
||||
val wifiName = when (networkStatus) {
|
||||
is NetworkStatus.Connected -> {
|
||||
networkStatus.wifiSsid
|
||||
}
|
||||
!connectivityState.wifiAvailable -> null
|
||||
else -> wifiName
|
||||
else -> null
|
||||
}
|
||||
copy(
|
||||
isWifiConnected = connectivityState.wifiAvailable,
|
||||
isMobileDataConnected = connectivityState.cellularAvailable,
|
||||
isEthernetConnected = isEthernetConnected,
|
||||
isWifiConnected = networkStatus.wifiConnected,
|
||||
isMobileDataConnected = networkStatus.cellularConnected,
|
||||
isEthernetConnected = networkStatus.ethernetConnected,
|
||||
wifiName = wifiName,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private fun startAutoTunnelStateJob() = lifecycleScope.launch(ioDispatcher) {
|
||||
combine(
|
||||
combineSettings(),
|
||||
networkMonitor.status.map {
|
||||
buildNetworkState(it)
|
||||
}.distinctUntilChanged(),
|
||||
appDataRepository.get().settings.flow
|
||||
.distinctUntilChanged { old, new -> old.isKernelEnabled == new.isKernelEnabled } // Only emit when isKernelEnabled changes
|
||||
.flatMapLatest { settings ->
|
||||
networkMonitor.getNetworkStatusFlow(true, settings.isKernelEnabled)
|
||||
.flowOn(ioDispatcher)
|
||||
.map { buildNetworkState(it) }
|
||||
}
|
||||
.distinctUntilChanged(),
|
||||
) { double, networkState ->
|
||||
AutoTunnelState(tunnelManager.activeTunnels().value, networkState, double.first, double.second)
|
||||
AutoTunnelState(
|
||||
tunnelManager.activeTunnels.value,
|
||||
networkState,
|
||||
double.first,
|
||||
double.second,
|
||||
)
|
||||
}.collect { state ->
|
||||
autoTunnelStateFlow.update {
|
||||
it.copy(activeTunnels = state.activeTunnels, networkState = state.networkState, settings = state.settings, tunnels = state.tunnels)
|
||||
it.copy(
|
||||
activeTunnels = state.activeTunnels,
|
||||
networkState = state.networkState,
|
||||
settings = state.settings,
|
||||
tunnels = state.tunnels,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getWifiName(wifiCapabilities: NetworkCapabilities): String? {
|
||||
val setting = appDataRepository.get().settings.get()
|
||||
return if (setting.isWifiNameByShellEnabled) {
|
||||
rootShell.get().getCurrentWifiName()
|
||||
} else {
|
||||
InternetConnectivityMonitor.getNetworkName(wifiCapabilities, this@AutoTunnelService)
|
||||
}
|
||||
}
|
||||
|
||||
private fun combineSettings(): Flow<Pair<AppSettings, Tunnels>> {
|
||||
return combine(
|
||||
appDataRepository.get().settings.flow,
|
||||
@@ -240,7 +239,7 @@ class AutoTunnelService : LifecycleService() {
|
||||
Timber.d("Starting with debounce delay of: ${settings.debounceDelaySeconds} seconds")
|
||||
autoTunnelStateFlow.debounce(settings.debounceDelayMillis()).collect { watcherState ->
|
||||
if (watcherState == defaultState) return@collect
|
||||
Timber.d("New auto tunnel state emitted")
|
||||
Timber.d("New auto tunnel state emitted ${watcherState.networkState}")
|
||||
when (val event = watcherState.asAutoTunnelEvent()) {
|
||||
is AutoTunnelEvent.Start -> (event.tunnelConf ?: appDataRepository.get().getPrimaryOrFirstTunnel())?.let {
|
||||
tunnelManager.startTunnel(it)
|
||||
|
||||
+9
-4
@@ -52,9 +52,14 @@ class TunnelControlTile : TileService() {
|
||||
}
|
||||
|
||||
fun updateTileState() = applicationScope.launch {
|
||||
if (appDataRepository.tunnels.getAll().isEmpty()) return@launch setUnavailable()
|
||||
with(tunnelManager.activeTunnels().value) {
|
||||
if (isNotEmpty()) return@launch updateTile(if (size == 1) first().tunName else getString(R.string.multiple), true)
|
||||
val tunnels = appDataRepository.tunnels.getAll()
|
||||
if (tunnels.isEmpty()) return@launch setUnavailable()
|
||||
with(tunnelManager.activeTunnels.value) {
|
||||
if (isNotEmpty()) if (size == 1) {
|
||||
tunnels.firstOrNull { it.id == keys.first() }?.let { return@launch updateTile(it.tunName, true) }
|
||||
} else {
|
||||
return@launch updateTile(getString(R.string.multiple), true)
|
||||
}
|
||||
}
|
||||
appDataRepository.getStartTunnelConfig()?.let {
|
||||
updateTile(it.tunName, false)
|
||||
@@ -65,7 +70,7 @@ class TunnelControlTile : TileService() {
|
||||
super.onClick()
|
||||
unlockAndRun {
|
||||
applicationScope.launch {
|
||||
if (tunnelManager.activeTunnels().value.isNotEmpty()) return@launch tunnelManager.stopTunnel()
|
||||
if (tunnelManager.activeTunnels.value.isNotEmpty()) return@launch tunnelManager.stopTunnel()
|
||||
appDataRepository.getStartTunnelConfig()?.let {
|
||||
tunnelManager.startTunnel(it)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||
import com.zaneschepke.networkmonitor.NetworkStatus
|
||||
import com.zaneschepke.wireguardautotunnel.R
|
||||
import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.core.network.NetworkMonitor
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.WireGuardNotification
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
@@ -14,10 +16,12 @@ 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.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.StringValue
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.cancelWithMessage
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -27,11 +31,12 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
open class BaseTunnel(
|
||||
@@ -45,93 +50,112 @@ open class BaseTunnel(
|
||||
|
||||
internal val tunnels = MutableStateFlow<List<TunnelConf>>(emptyList())
|
||||
|
||||
private val tunnelJobs = mutableMapOf<TunnelConf, Job>()
|
||||
private val _tunnelStates = MutableStateFlow<Map<Int, TunnelState>>(emptyMap())
|
||||
|
||||
private val tunnelJobs = ConcurrentHashMap<Int, Job>()
|
||||
|
||||
private val isNetworkAvailable = AtomicBoolean(false)
|
||||
|
||||
init {
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
launch {
|
||||
startNetworkJob()
|
||||
}
|
||||
launch { startNetworkJob() }
|
||||
launch { monitorTunnelConfigChanges() }
|
||||
tunnels.collect { tuns ->
|
||||
val previousTuns = tunnelJobs.keys.toSet()
|
||||
val newTuns = tuns - previousTuns
|
||||
val removedItems = previousTuns - tuns.toSet()
|
||||
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}")
|
||||
tunnelJobs[tun] = startTunnelJobs(tun)
|
||||
Timber.d("Starting tunnel jobs for tun ${tun.name} (ID: ${tun.id})")
|
||||
tunnelJobs[tun.id] = startTunnelJobs(tun)
|
||||
}
|
||||
|
||||
removedItems.forEach { tun ->
|
||||
tunnelJobs[tun]?.cancelWithMessage("Canceling tunnel jobs for tunnel: ${tun.name}")
|
||||
tunnelJobs.remove(tun)
|
||||
removedTunIds.forEach { tunId ->
|
||||
tunnelJobs[tunId]?.cancelWithMessage("Canceling tunnel jobs for tunnel ID: $tunId")
|
||||
tunnelJobs.remove(tunId)
|
||||
_tunnelStates.update { it - tunId }
|
||||
serviceManager.updateTunnelTile()
|
||||
}
|
||||
serviceManager.updateTunnelTile()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startTunnelJobs(tunnel: TunnelConf) = applicationScope.launch(ioDispatcher) {
|
||||
launch {
|
||||
startTunnelStatisticsJob(tunnel)
|
||||
launch { startTunnelStatisticsJob(tunnel) }
|
||||
if (tunnel.isPingEnabled) launch { startPingJob(tunnel) }
|
||||
}
|
||||
|
||||
private fun updateTunnelState(tunnelId: Int, newState: TunnelStatus) {
|
||||
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
|
||||
}
|
||||
launch {
|
||||
startPingJob(tunnel)
|
||||
}
|
||||
|
||||
internal fun beforeStartTunnel(tunnelConf: TunnelConf) {
|
||||
tunnelConf.setStateChangeCallback { state ->
|
||||
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()
|
||||
}
|
||||
}
|
||||
launch {
|
||||
startTunnelConfigChangeJob(tunnel)
|
||||
}
|
||||
|
||||
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) {
|
||||
if (tunnels.value.any { it.id == tunnelConf.id }) {
|
||||
toggleTunnel(tunnelConf, TunnelStatus.DOWN)
|
||||
toggleTunnel(tunnelConf, TunnelStatus.UP)
|
||||
}
|
||||
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 suspend fun activeTunnels(): StateFlow<List<TunnelConf>> {
|
||||
return tunnels.asStateFlow()
|
||||
}
|
||||
|
||||
override suspend fun startTunnel(tunnelConf: TunnelConf) {
|
||||
if (tunnels.value.any { it.id == tunnelConf.id }) return Timber.w("Tunnel already running")
|
||||
serviceManager.startBackgroundService(tunnelConf)
|
||||
appDataRepository.tunnels.save(tunnelConf.copy(isActive = true))
|
||||
}
|
||||
|
||||
override suspend fun stopTunnel(tunnelConf: TunnelConf?) {
|
||||
}
|
||||
|
||||
open suspend fun toggleTunnel(tunnelConf: TunnelConf, state: TunnelStatus) {
|
||||
}
|
||||
|
||||
open suspend fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics {
|
||||
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 suspend fun stopAllTunnels() {
|
||||
internal fun stopAllTunnels() {
|
||||
tunnels.value.forEach {
|
||||
stopTunnel(it)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun addToActiveTunnels(conf: TunnelConf) {
|
||||
private fun addToActiveTunnels(conf: TunnelConf) {
|
||||
tunnels.update {
|
||||
it.toMutableList().apply {
|
||||
add(conf)
|
||||
@@ -148,23 +172,26 @@ open class BaseTunnel(
|
||||
}
|
||||
|
||||
private suspend fun startNetworkJob() = coroutineScope {
|
||||
networkMonitor.status.distinctUntilChanged().collect {
|
||||
isNetworkAvailable.set(!it.allOffline)
|
||||
}
|
||||
networkMonitor.getNetworkStatusFlow(includeWifiSsid = false, useRootShell = false)
|
||||
.flowOn(ioDispatcher).collect {
|
||||
isNetworkAvailable.set(it !is NetworkStatus.Disconnected)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun startPingJob(tunnel: TunnelConf) = coroutineScope {
|
||||
while (isActive) {
|
||||
if (isNetworkAvailable.get() && tunnel.isActive) {
|
||||
val pingResult = tunnel.pingTunnel(ioDispatcher)
|
||||
handlePingResult(tunnel, pingResult)
|
||||
runCatching {
|
||||
if (isNetworkAvailable.get() && tunnel.isActive) {
|
||||
val pingSuccess = tunnel.isTunnelPingable(ioDispatcher)
|
||||
handlePingResult(tunnel, pingSuccess)
|
||||
}
|
||||
delay(tunnel.pingInterval ?: Constants.PING_INTERVAL)
|
||||
}
|
||||
delay(CHECK_INTERVAL)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handlePingResult(tunnel: TunnelConf, pingResult: List<Boolean>) {
|
||||
if (pingResult.contains(false)) {
|
||||
private suspend fun handlePingResult(tunnel: TunnelConf, pingSuccess: Boolean) {
|
||||
if (!pingSuccess) {
|
||||
if (isNetworkAvailable.get()) {
|
||||
Timber.i("Ping result: target was not reachable, bouncing the tunnel")
|
||||
bounceTunnel(tunnel)
|
||||
@@ -197,23 +224,33 @@ open class BaseTunnel(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun startTunnelConfigChangeJob(tunnel: TunnelConf) = coroutineScope {
|
||||
private suspend fun monitorTunnelConfigChanges() = coroutineScope {
|
||||
appDataRepository.tunnels.flow.collect { storageTuns ->
|
||||
storageTuns.firstOrNull { it.id == tunnel.id }?.let { storageTun ->
|
||||
if (tunnel.isQuickConfigChanged(storageTun) || tunnel.isPingConfigMatching(storageTun)) {
|
||||
bounceTunnel(tunnel)
|
||||
storageTuns.forEach { storageTun ->
|
||||
val currentTun = tunnels.value.firstOrNull { it.id == storageTun.id }
|
||||
if (currentTun != null) {
|
||||
if (!currentTun.isQuickConfigMatching(storageTun)) {
|
||||
Timber.d("Tunnel config changed for ID $storageTun, bouncing tunnel")
|
||||
bounceTunnel(storageTun)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun startTunnelStatisticsJob(tunnel: TunnelConf) = coroutineScope {
|
||||
while (isActive) {
|
||||
val stats = getStatistics(tunnel)
|
||||
tunnel.state.update {
|
||||
it.copy(statistics = stats)
|
||||
while (this.isActive) {
|
||||
runCatching {
|
||||
val stats = getStatistics(tunnel)
|
||||
_tunnelStates.update { currentStates ->
|
||||
val updatedState = currentStates[tunnel.id]?.copy(statistics = stats)
|
||||
?: TunnelState(statistics = stats)
|
||||
currentStates + (tunnel.id to updatedState)
|
||||
}
|
||||
delay(CHECK_INTERVAL)
|
||||
}.onFailure { exception ->
|
||||
Timber.e(exception, "Failed to update tunnel statistics for ${tunnel.tunName}")
|
||||
}
|
||||
delay(CHECK_INTERVAL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,21 +3,20 @@ package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||
import com.wireguard.android.backend.Backend
|
||||
import com.wireguard.android.backend.BackendException
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
import com.zaneschepke.wireguardautotunnel.core.network.NetworkMonitor
|
||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||
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.state.TunnelStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.WireGuardStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -31,13 +30,18 @@ class KernelTunnel @Inject constructor(
|
||||
networkMonitor: NetworkMonitor,
|
||||
) : BaseTunnel(ioDispatcher, applicationScope, networkMonitor, appDataRepository, serviceManager, notificationManager) {
|
||||
|
||||
override suspend fun startTunnel(tunnelConf: TunnelConf) {
|
||||
withContext(ioDispatcher) {
|
||||
super.startTunnel(tunnelConf)
|
||||
override fun startTunnel(tunnelConf: TunnelConf) {
|
||||
Timber.d("Starting tunnel ${tunnelConf.id} kernel")
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
if (tunnels.value.any { it.id == tunnelConf.id }) return@launch Timber.w("Tunnel already running")
|
||||
runCatching {
|
||||
Timber.d("Setting backend state UP")
|
||||
super.beforeStartTunnel(tunnelConf)
|
||||
backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toWgConfig())
|
||||
addToActiveTunnels(tunnelConf)
|
||||
Timber.d("Calling super.startTunnel")
|
||||
super.startTunnel(tunnelConf)
|
||||
}.onFailure {
|
||||
Timber.e(it, "Failed to start tunnel ${tunnelConf.id} kernel")
|
||||
onTunnelStop(tunnelConf)
|
||||
if (it is BackendException) {
|
||||
handleBackendThrowable(it.toBackendError())
|
||||
@@ -48,15 +52,14 @@ class KernelTunnel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics {
|
||||
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics {
|
||||
return WireGuardStatistics(backend.getStatistics(tunnelConf))
|
||||
}
|
||||
|
||||
override suspend fun stopTunnel(tunnelConf: TunnelConf?) {
|
||||
withContext(ioDispatcher) {
|
||||
val tunnel = tunnels.value.firstOrNull { it.id == tunnelConf?.id }
|
||||
override fun stopTunnel(tunnelConf: TunnelConf?) {
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
runCatching {
|
||||
tunnel?.let {
|
||||
tunnels.value.firstOrNull { it.id == tunnelConf?.id }?.let {
|
||||
backend.setState(it, Tunnel.State.DOWN, it.toWgConfig())
|
||||
onTunnelStop(it)
|
||||
} ?: stopAllTunnels()
|
||||
@@ -66,13 +69,6 @@ class KernelTunnel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun toggleTunnel(tunnelConf: TunnelConf, status: TunnelStatus) {
|
||||
when (status) {
|
||||
TunnelStatus.UP -> backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toWgConfig())
|
||||
TunnelStatus.DOWN -> backend.setState(tunnelConf, Tunnel.State.DOWN, tunnelConf.toWgConfig())
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
|
||||
Timber.w("Not yet implemented for kernel")
|
||||
}
|
||||
|
||||
+51
-53
@@ -4,20 +4,20 @@ import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.di.Kernel
|
||||
import com.zaneschepke.wireguardautotunnel.di.Userspace
|
||||
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.repository.AppDataRepository
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.withData
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.plus
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class TunnelManager @Inject constructor(
|
||||
@@ -28,71 +28,69 @@ class TunnelManager @Inject constructor(
|
||||
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
|
||||
) : TunnelProvider {
|
||||
|
||||
val appSettings: StateFlow<AppSettings?> = appDataRepository.settings.flow.stateIn(
|
||||
scope = applicationScope.plus(ioDispatcher),
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = null,
|
||||
)
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val tunnelProviderFlow = appDataRepository.settings.flow
|
||||
.filterNotNull()
|
||||
.flatMapLatest { settings ->
|
||||
MutableStateFlow(if (settings.isKernelEnabled) kernelTunnel else userspaceTunnel)
|
||||
}
|
||||
.stateIn(
|
||||
scope = applicationScope.plus(ioDispatcher),
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = userspaceTunnel,
|
||||
)
|
||||
|
||||
override suspend fun activeTunnels(): StateFlow<List<TunnelConf>> {
|
||||
return withContext(ioDispatcher) {
|
||||
appSettings.filterNotNull().first().let {
|
||||
if (it.isKernelEnabled) return@withContext kernelTunnel.activeTunnels()
|
||||
userspaceTunnel.activeTunnels()
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override val activeTunnels = appDataRepository.settings.flow
|
||||
.filterNotNull()
|
||||
.flatMapLatest { settings ->
|
||||
if (settings.isKernelEnabled) {
|
||||
kernelTunnel.activeTunnels
|
||||
} else {
|
||||
userspaceTunnel.activeTunnels
|
||||
}
|
||||
}
|
||||
.stateIn(
|
||||
scope = applicationScope.plus(ioDispatcher),
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = emptyMap(),
|
||||
)
|
||||
|
||||
override fun startTunnel(tunnelConf: TunnelConf) {
|
||||
tunnelProviderFlow.value.startTunnel(tunnelConf)
|
||||
}
|
||||
|
||||
override suspend fun startTunnel(tunnelConf: TunnelConf) {
|
||||
appSettings.withData {
|
||||
if (it.isKernelEnabled) return@withData kernelTunnel.startTunnel(tunnelConf)
|
||||
userspaceTunnel.startTunnel(tunnelConf)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun stopTunnel(tunnelConf: TunnelConf?) {
|
||||
appSettings.withData {
|
||||
if (it.isKernelEnabled) return@withData kernelTunnel.stopTunnel(tunnelConf)
|
||||
userspaceTunnel.stopTunnel(tunnelConf)
|
||||
}
|
||||
override fun stopTunnel(tunnelConf: TunnelConf?) {
|
||||
tunnelProviderFlow.value.stopTunnel(tunnelConf)
|
||||
}
|
||||
|
||||
override suspend fun bounceTunnel(tunnelConf: TunnelConf) {
|
||||
appSettings.withData {
|
||||
if (it.isKernelEnabled) return@withData kernelTunnel.stopTunnel(tunnelConf)
|
||||
userspaceTunnel.stopTunnel(tunnelConf)
|
||||
}
|
||||
tunnelProviderFlow.value.bounceTunnel(tunnelConf)
|
||||
}
|
||||
|
||||
override suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>) {
|
||||
appSettings.withData {
|
||||
if (it.isKernelEnabled) return@withData kernelTunnel.setBackendState(backendState, allowedIps)
|
||||
userspaceTunnel.setBackendState(backendState, allowedIps)
|
||||
}
|
||||
tunnelProviderFlow.value.setBackendState(backendState, allowedIps)
|
||||
}
|
||||
|
||||
override suspend fun runningTunnelNames(): Set<String> {
|
||||
appSettings.filterNotNull().first().let {
|
||||
if (it.isKernelEnabled) return kernelTunnel.runningTunnelNames()
|
||||
return userspaceTunnel.runningTunnelNames()
|
||||
}
|
||||
return tunnelProviderFlow.value.runningTunnelNames()
|
||||
}
|
||||
|
||||
suspend fun restorePreviousState() {
|
||||
withContext(ioDispatcher) {
|
||||
with(appDataRepository.settings.get()) {
|
||||
if (isRestoreOnBootEnabled) {
|
||||
val previouslyActiveTuns = appDataRepository.tunnels.getActive()
|
||||
// handle kernel mode
|
||||
val tunsToStart = previouslyActiveTuns.filterNot { tun -> activeTunnels().value.any { tun.id == it.id } }
|
||||
if (isKernelEnabled) {
|
||||
return@withContext tunsToStart.forEach {
|
||||
startTunnel(it)
|
||||
}
|
||||
}
|
||||
// handle userspace
|
||||
if (activeTunnels().value.isEmpty()) tunsToStart.firstOrNull()?.let { startTunnel(it) }
|
||||
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics {
|
||||
return tunnelProviderFlow.value.getStatistics(tunnelConf)
|
||||
}
|
||||
|
||||
fun restorePreviousState() = applicationScope.launch(ioDispatcher) {
|
||||
val settings = appDataRepository.settings.get()
|
||||
if (settings.isRestoreOnBootEnabled) {
|
||||
val previouslyActiveTuns = appDataRepository.tunnels.getActive()
|
||||
val tunsToStart = previouslyActiveTuns.filterNot { tun -> activeTunnels.value.any { tun.id == it.key } }
|
||||
if (settings.isKernelEnabled) {
|
||||
return@launch tunsToStart.forEach {
|
||||
startTunnel(it)
|
||||
}
|
||||
} else {
|
||||
tunsToStart.firstOrNull()?.let { startTunnel(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-14
@@ -2,23 +2,20 @@ package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
interface TunnelProvider {
|
||||
|
||||
suspend fun activeTunnels(): StateFlow<List<TunnelConf>>
|
||||
|
||||
suspend fun startTunnel(tunnelConf: TunnelConf)
|
||||
|
||||
suspend fun stopTunnel(tunnelConf: TunnelConf? = null)
|
||||
|
||||
suspend fun bounceTunnel(tunnelConf: TunnelConf)
|
||||
|
||||
suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>)
|
||||
|
||||
suspend fun runningTunnelNames(): Set<String>
|
||||
|
||||
companion object {
|
||||
const val CHECK_INTERVAL = 1_000L
|
||||
const val CHECK_INTERVAL = 1000L
|
||||
}
|
||||
|
||||
fun startTunnel(tunnelConf: TunnelConf)
|
||||
fun stopTunnel(tunnelConf: TunnelConf? = null)
|
||||
suspend fun bounceTunnel(tunnelConf: TunnelConf)
|
||||
suspend fun setBackendState(backendState: BackendState, allowedIps: Collection<String>)
|
||||
suspend fun runningTunnelNames(): Set<String>
|
||||
fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics
|
||||
val activeTunnels: StateFlow<Map<Int, TunnelState>>
|
||||
}
|
||||
|
||||
+22
-21
@@ -1,14 +1,13 @@
|
||||
package com.zaneschepke.wireguardautotunnel.core.tunnel
|
||||
|
||||
import com.wireguard.android.backend.BackendException
|
||||
import com.zaneschepke.wireguardautotunnel.core.network.NetworkMonitor
|
||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.di.ApplicationScope
|
||||
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
|
||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||
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.state.AmneziaStatistics
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelStatistics
|
||||
@@ -16,7 +15,7 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.asAmBackendState
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.toBackendError
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.launch
|
||||
import org.amnezia.awg.backend.Backend
|
||||
import org.amnezia.awg.backend.Tunnel
|
||||
import timber.log.Timber
|
||||
@@ -30,15 +29,24 @@ class UserspaceTunnel @Inject constructor(
|
||||
notificationManager: NotificationManager,
|
||||
private val backend: Backend,
|
||||
networkMonitor: NetworkMonitor,
|
||||
) : TunnelProvider, BaseTunnel(ioDispatcher, applicationScope, networkMonitor, appDataRepository, serviceManager, notificationManager) {
|
||||
) : BaseTunnel(ioDispatcher, applicationScope, networkMonitor, appDataRepository, serviceManager, notificationManager) {
|
||||
|
||||
override suspend fun startTunnel(tunnelConf: TunnelConf) {
|
||||
withContext(ioDispatcher) {
|
||||
super.startTunnel(tunnelConf)
|
||||
override fun startTunnel(tunnelConf: TunnelConf) {
|
||||
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 {
|
||||
Timber.d("Setting backend state UP")
|
||||
super.beforeStartTunnel(tunnelConf)
|
||||
backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toAmConfig())
|
||||
addToActiveTunnels(tunnelConf)
|
||||
Timber.d("Calling super.startTunnel")
|
||||
super.startTunnel(tunnelConf)
|
||||
}.onFailure {
|
||||
Timber.e(it, "Failed to start tunnel ${tunnelConf.id} userspace")
|
||||
onTunnelStop(tunnelConf)
|
||||
if (it is BackendException) {
|
||||
handleBackendThrowable(it.toBackendError())
|
||||
@@ -49,19 +57,8 @@ class UserspaceTunnel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics {
|
||||
return AmneziaStatistics(backend.getStatistics(tunnelConf))
|
||||
}
|
||||
|
||||
override suspend fun toggleTunnel(tunnelConf: TunnelConf, status: TunnelStatus) {
|
||||
when (status) {
|
||||
TunnelStatus.UP -> backend.setState(tunnelConf, Tunnel.State.UP, tunnelConf.toAmConfig())
|
||||
TunnelStatus.DOWN -> backend.setState(tunnelConf, Tunnel.State.DOWN, tunnelConf.toAmConfig())
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun stopTunnel(tunnelConf: TunnelConf?) {
|
||||
withContext(ioDispatcher) {
|
||||
override fun stopTunnel(tunnelConf: TunnelConf?) {
|
||||
applicationScope.launch(ioDispatcher) {
|
||||
runCatching {
|
||||
tunnels.value.firstOrNull { it.id == tunnelConf?.id }?.let {
|
||||
backend.setState(it, Tunnel.State.DOWN, it.toAmConfig())
|
||||
@@ -80,4 +77,8 @@ class UserspaceTunnel @Inject constructor(
|
||||
override suspend fun runningTunnelNames(): Set<String> {
|
||||
return backend.runningTunnelNames
|
||||
}
|
||||
|
||||
override fun getStatistics(tunnelConf: TunnelConf): TunnelStatistics {
|
||||
return AmneziaStatistics(backend.getStatistics(tunnelConf))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ class ServiceWorker @AssistedInject constructor(
|
||||
Timber.i("Service worker started")
|
||||
with(appDataRepository.settings.get()) {
|
||||
if (isAutoTunnelEnabled && !serviceManager.autoTunnelActive.value) return@with serviceManager.startAutoTunnel(true)
|
||||
if (tunnelManager.activeTunnels().value.isEmpty()) tunnelManager.restorePreviousState()
|
||||
if (tunnelManager.activeTunnels.value.isEmpty()) tunnelManager.restorePreviousState()
|
||||
}
|
||||
Result.success()
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
package com.zaneschepke.wireguardautotunnel.di
|
||||
|
||||
import com.zaneschepke.wireguardautotunnel.core.network.InternetConnectivityMonitor
|
||||
import com.zaneschepke.wireguardautotunnel.core.network.NetworkMonitor
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
abstract class ServiceModule {
|
||||
|
||||
@Binds
|
||||
abstract fun provideInternetConnectivityService(wifiService: InternetConnectivityMonitor): NetworkMonitor
|
||||
}
|
||||
@@ -4,7 +4,8 @@ import android.content.Context
|
||||
import com.wireguard.android.backend.WgQuickBackend
|
||||
import com.wireguard.android.util.RootShell
|
||||
import com.wireguard.android.util.ToolsInstaller
|
||||
import com.zaneschepke.wireguardautotunnel.core.network.NetworkMonitor
|
||||
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor
|
||||
import com.zaneschepke.networkmonitor.NetworkMonitor
|
||||
import com.zaneschepke.wireguardautotunnel.core.notification.NotificationManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.service.ServiceManager
|
||||
import com.zaneschepke.wireguardautotunnel.core.tunnel.KernelTunnel
|
||||
@@ -98,13 +99,20 @@ class TunnelModule {
|
||||
return TunnelManager(kernelTunnel, userspaceTunnel, appDataRepository, applicationScope, ioDispatcher)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideNetworkMonitor(@ApplicationContext context: Context): NetworkMonitor {
|
||||
return AndroidNetworkMonitor(context)
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideServiceManager(
|
||||
@ApplicationContext context: Context,
|
||||
@IoDispatcher ioDispatcher: CoroutineDispatcher,
|
||||
@ApplicationScope applicationScope: CoroutineScope,
|
||||
appDataRepository: AppDataRepository,
|
||||
): ServiceManager {
|
||||
return ServiceManager(context, ioDispatcher, appDataRepository)
|
||||
return ServiceManager(context, ioDispatcher, applicationScope, appDataRepository)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
package com.zaneschepke.wireguardautotunnel.domain.entity
|
||||
|
||||
import com.wireguard.config.Config
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.asTunnelState
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isReachable
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.toWgQuickString
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.Transient
|
||||
import org.amnezia.awg.backend.Tunnel
|
||||
import timber.log.Timber
|
||||
import java.io.InputStream
|
||||
@@ -30,9 +27,13 @@ data class TunnelConf(
|
||||
val pingIp: String? = null,
|
||||
val isEthernetTunnel: Boolean = false,
|
||||
val isIpv4Preferred: Boolean = false,
|
||||
@Transient
|
||||
private var stateChangeCallback: ((Any) -> Unit)? = null,
|
||||
) : Tunnel, com.wireguard.android.backend.Tunnel {
|
||||
|
||||
val state = MutableStateFlow(TunnelState())
|
||||
fun setStateChangeCallback(callback: (Any) -> Unit) {
|
||||
stateChangeCallback = callback
|
||||
}
|
||||
|
||||
fun toAmConfig(): org.amnezia.awg.config.Config {
|
||||
return configFromAmQuick(amQuick.ifBlank { wgQuick })
|
||||
@@ -50,21 +51,17 @@ data class TunnelConf(
|
||||
return isIpv4Preferred
|
||||
}
|
||||
|
||||
override fun onStateChange(newState: Tunnel.State) {
|
||||
state.update {
|
||||
it.copy(state = newState.asTunnelState())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStateChange(newState: com.wireguard.android.backend.Tunnel.State) {
|
||||
state.update {
|
||||
it.copy(state = newState.asTunnelState())
|
||||
}
|
||||
stateChangeCallback?.invoke(newState)
|
||||
}
|
||||
|
||||
fun isQuickConfigChanged(updatedConf: TunnelConf): Boolean {
|
||||
return updatedConf.wgQuick != wgQuick ||
|
||||
updatedConf.amQuick != amQuick
|
||||
override fun onStateChange(newState: Tunnel.State) {
|
||||
stateChangeCallback?.invoke(newState)
|
||||
}
|
||||
|
||||
fun isQuickConfigMatching(updatedConf: TunnelConf): Boolean {
|
||||
return updatedConf.wgQuick == wgQuick ||
|
||||
updatedConf.amQuick == amQuick
|
||||
}
|
||||
|
||||
fun isPingConfigMatching(updatedConf: TunnelConf): Boolean {
|
||||
@@ -74,18 +71,17 @@ data class TunnelConf(
|
||||
updatedConf.pingInterval == pingInterval
|
||||
}
|
||||
|
||||
suspend fun pingTunnel(context: CoroutineContext): List<Boolean> {
|
||||
suspend fun isTunnelPingable(context: CoroutineContext): Boolean {
|
||||
return withContext(context) {
|
||||
val config = toWgConfig()
|
||||
if (pingIp != null) {
|
||||
Timber.i("Pinging custom ip")
|
||||
listOf(InetAddress.getByName(pingIp).isReachable(Constants.PING_TIMEOUT.toInt()))
|
||||
} else {
|
||||
Timber.i("Pinging all peers")
|
||||
config.peers.map { peer ->
|
||||
peer.isReachable(isIpv4Preferred)
|
||||
}
|
||||
return@withContext InetAddress.getByName(pingIp)
|
||||
.isReachable(Constants.PING_TIMEOUT.toInt())
|
||||
}
|
||||
Timber.i("Pinging all peers")
|
||||
config.peers.map { peer ->
|
||||
peer.isReachable(isIpv4Preferred)
|
||||
}.all { true }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-5
@@ -7,7 +7,7 @@ import com.zaneschepke.wireguardautotunnel.domain.events.AutoTunnelEvent
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isMatchingToWildcardList
|
||||
|
||||
data class AutoTunnelState(
|
||||
val activeTunnels: List<TunnelConf> = emptyList(),
|
||||
val activeTunnels: Map<Int, TunnelState> = emptyMap(),
|
||||
val networkState: NetworkState = NetworkState(),
|
||||
val settings: AppSettings = AppSettings(),
|
||||
val tunnels: List<TunnelConf> = emptyList(),
|
||||
@@ -20,12 +20,12 @@ data class AutoTunnelState(
|
||||
private fun isMobileTunnelDataChangeNeeded(): Boolean {
|
||||
val preferredTunnel = preferredMobileDataTunnel()
|
||||
return preferredTunnel != null &&
|
||||
activeTunnels.isNotEmpty() && !activeTunnels.any { it.id == preferredTunnel.id }
|
||||
activeTunnels.isNotEmpty() && !activeTunnels.any { it.key == preferredTunnel.id }
|
||||
}
|
||||
|
||||
private fun isEthernetTunnelChangeNeeded(): Boolean {
|
||||
val preferredTunnel = preferredEthernetTunnel()
|
||||
return preferredTunnel != null && activeTunnels.isNotEmpty() && !activeTunnels.any { it.id == preferredTunnel.id }
|
||||
return preferredTunnel != null && activeTunnels.isNotEmpty() && !activeTunnels.any { it.key == preferredTunnel.id }
|
||||
}
|
||||
|
||||
private fun preferredMobileDataTunnel(): TunnelConf? {
|
||||
@@ -62,7 +62,7 @@ data class AutoTunnelState(
|
||||
return settings.isVpnKillSwitchEnabled && (!settings.isDisableKillSwitchOnTrustedEnabled || !isCurrentSSIDTrusted())
|
||||
}
|
||||
|
||||
fun isNoConnectivity(): Boolean {
|
||||
private fun isNoConnectivity(): Boolean {
|
||||
return !networkState.isEthernetConnected && !networkState.isWifiConnected && !networkState.isMobileDataConnected
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ data class AutoTunnelState(
|
||||
|
||||
private fun isWifiTunnelPreferred(): Boolean {
|
||||
val preferred = preferredWifiTunnel()
|
||||
return activeTunnels.any { it.id == preferred?.id }
|
||||
return activeTunnels.any { it.key == preferred?.id }
|
||||
}
|
||||
|
||||
fun asAutoTunnelEvent(): AutoTunnelEvent {
|
||||
|
||||
+7
-13
@@ -77,7 +77,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
|
||||
var selectedTunnel by remember { mutableStateOf<TunnelConf?>(null) }
|
||||
val isRunningOnTv = remember { context.isRunningOnTv() }
|
||||
|
||||
val activeTunnels by viewModel.activeTunnels.collectAsStateWithLifecycle(emptyList())
|
||||
val activeTunnels by viewModel.tunnelManager.activeTunnels.collectAsStateWithLifecycle(emptyMap())
|
||||
|
||||
val collator = Collator.getInstance(Locale.getDefault())
|
||||
|
||||
@@ -89,6 +89,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
|
||||
val startTunnel = withVpnPermission<TunnelConf> {
|
||||
viewModel.onTunnelStart(it)
|
||||
}
|
||||
|
||||
val autoTunnelToggleBattery = withIgnoreBatteryOpt(uiState.generalState.isBatteryOptimizationDisableShown) {
|
||||
if (!uiState.generalState.isBatteryOptimizationDisableShown) viewModel.setBatteryOptimizeDisableShown()
|
||||
if (uiState.appSettings.isKernelEnabled) {
|
||||
@@ -132,15 +133,8 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
|
||||
}
|
||||
|
||||
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConf) {
|
||||
if (!checked) {
|
||||
viewModel.onTunnelStop(tunnel)
|
||||
return
|
||||
}
|
||||
if (uiState.appSettings.isKernelEnabled) {
|
||||
viewModel.onTunnelStart(tunnel)
|
||||
} else {
|
||||
startTunnel.invoke(tunnel)
|
||||
}
|
||||
if (!checked) return viewModel.onTunnelStop(tunnel).let { }
|
||||
if (uiState.appSettings.isKernelEnabled) viewModel.onTunnelStart(tunnel) else startTunnel(tunnel)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
@@ -233,13 +227,13 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState)
|
||||
key = { tunnel -> tunnel.id },
|
||||
) { tunnel ->
|
||||
val expanded = uiState.generalState.isTunnelStatsExpanded
|
||||
val tunnelState = activeTunnels.firstOrNull { it.id == tunnel.id }?.state?.collectAsStateWithLifecycle()
|
||||
val tunnelState = activeTunnels.getOrDefault(tunnel.id, TunnelState())
|
||||
TunnelRowItem(
|
||||
tunnel.isActive,
|
||||
tunnelState.state.isUp(),
|
||||
expanded,
|
||||
selectedTunnel?.id == tunnel.id,
|
||||
tunnel,
|
||||
tunnelState = tunnelState?.value ?: TunnelState(),
|
||||
tunnelState = tunnelState,
|
||||
{ selectedTunnel = tunnel },
|
||||
{ viewModel.onExpandedChanged(!expanded) },
|
||||
onDelete = { showDeleteTunnelAlertDialog = true },
|
||||
|
||||
+3
-1
@@ -41,6 +41,7 @@ import com.zaneschepke.wireguardautotunnel.ui.common.config.SubmitConfigurationT
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.TopNavBar
|
||||
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.ForwardButton
|
||||
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
|
||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isValidIpv4orIpv6Address
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.scaledHeight
|
||||
@@ -51,7 +52,7 @@ import kotlin.text.isNullOrBlank
|
||||
import kotlin.text.toLong
|
||||
|
||||
@Composable
|
||||
fun OptionsScreen(tunnelConf: TunnelConf, viewModel: TunnelOptionsViewModel = hiltViewModel()) {
|
||||
fun OptionsScreen(tunnelConf: TunnelConf, appUiState: AppUiState, viewModel: TunnelOptionsViewModel = hiltViewModel()) {
|
||||
val navController = LocalNavController.current
|
||||
|
||||
var currentText by remember { mutableStateOf("") }
|
||||
@@ -194,6 +195,7 @@ fun OptionsScreen(tunnelConf: TunnelConf, viewModel: TunnelOptionsViewModel = hi
|
||||
trailing = {
|
||||
ScaledSwitch(
|
||||
checked = tunnelConf.isPingEnabled,
|
||||
enabled = !appUiState.activeTunnels.containsKey(tunnelConf.id),
|
||||
onClick = { onPingToggle() },
|
||||
)
|
||||
},
|
||||
|
||||
+5
-5
@@ -352,11 +352,11 @@ fun SettingsScreen(viewModel: SettingsViewModel = hiltViewModel(), appViewModel:
|
||||
ScaledSwitch(
|
||||
uiState.appSettings.isKernelEnabled,
|
||||
onClick = { appViewModel.onToggleKernelMode() },
|
||||
// enabled = !(
|
||||
// uiState.settings.isAutoTunnelEnabled ||
|
||||
// uiState.settings.isAlwaysOnVpnEnabled ||
|
||||
// (uiState.vpnState.status == TunnelState.UP)
|
||||
// ),
|
||||
enabled = !(
|
||||
uiState.appSettings.isAutoTunnelEnabled ||
|
||||
uiState.appSettings.isAlwaysOnVpnEnabled ||
|
||||
uiState.activeTunnels.isNotEmpty()
|
||||
),
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
|
||||
@@ -3,10 +3,12 @@ package com.zaneschepke.wireguardautotunnel.ui.state
|
||||
import com.zaneschepke.wireguardautotunnel.data.model.GeneralState
|
||||
import com.zaneschepke.wireguardautotunnel.domain.entity.AppSettings
|
||||
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
|
||||
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
|
||||
|
||||
data class AppUiState(
|
||||
val appSettings: AppSettings = AppSettings(),
|
||||
val tunnels: List<TunnelConf> = emptyList(),
|
||||
val activeTunnels: Map<Int, TunnelState> = emptyMap(),
|
||||
val generalState: GeneralState = GeneralState(),
|
||||
val autoTunnelActive: Boolean = false,
|
||||
)
|
||||
|
||||
+2
-9
@@ -2,7 +2,6 @@ package com.zaneschepke.wireguardautotunnel.util.extensions
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.wireguard.android.backend.BackendException
|
||||
import com.wireguard.android.util.RootShell
|
||||
import com.wireguard.config.Peer
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendError
|
||||
import com.zaneschepke.wireguardautotunnel.domain.enums.BackendState
|
||||
@@ -46,9 +45,9 @@ fun TunnelStatistics.PeerStats.handshakeStatus(): HandshakeStatus {
|
||||
fun Peer.isReachable(preferIpv4: Boolean): Boolean {
|
||||
val host =
|
||||
if (this.endpoint.isPresent &&
|
||||
this.endpoint.get().getResolved(preferIpv4).isPresent
|
||||
this.endpoint.get().resolved.isPresent
|
||||
) {
|
||||
this.endpoint.get().getResolved(preferIpv4).get().host
|
||||
this.endpoint.get().resolved.get().host
|
||||
} else {
|
||||
Constants.DEFAULT_PING_IP
|
||||
}
|
||||
@@ -87,12 +86,6 @@ fun Config.toWgQuickString(): String {
|
||||
return lines.joinToString(System.lineSeparator())
|
||||
}
|
||||
|
||||
fun RootShell.getCurrentWifiName(): String? {
|
||||
val response = mutableListOf<String>()
|
||||
this.run(response, "dumpsys wifi | grep 'Supplicant state: COMPLETED' | grep -o 'SSID: [^,]*' | cut -d ' ' -f2- | tr -d '\"'")
|
||||
return response.firstOrNull()
|
||||
}
|
||||
|
||||
fun Backend.BackendState.asBackendState(): BackendState {
|
||||
return BackendState.valueOf(this.name)
|
||||
}
|
||||
|
||||
@@ -8,12 +8,6 @@ import com.zaneschepke.wireguardautotunnel.WireGuardAutoTunnel
|
||||
import com.zaneschepke.wireguardautotunnel.ui.Route
|
||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.isCurrentRoute
|
||||
|
||||
fun NavController.navigateAndForget(route: Route) {
|
||||
navigate(route) {
|
||||
popUpTo(0)
|
||||
}
|
||||
}
|
||||
|
||||
fun NavController.goFromRoot(route: Route) {
|
||||
if (currentBackStackEntry?.isCurrentRoute(route::class) == true) return
|
||||
this.navigate(route) {
|
||||
|
||||
@@ -31,11 +31,9 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.takeWhile
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.plus
|
||||
@@ -75,11 +73,13 @@ constructor(
|
||||
appDataRepository.settings.flow,
|
||||
appDataRepository.tunnels.flow,
|
||||
appDataRepository.appState.flow,
|
||||
tunnelManager.activeTunnels,
|
||||
serviceManager.autoTunnelActive,
|
||||
) { settings, tunnels, generalState, autoTunnel ->
|
||||
) { settings, tunnels, generalState, activeTunnels, autoTunnel ->
|
||||
AppUiState(
|
||||
settings,
|
||||
tunnels,
|
||||
activeTunnels,
|
||||
generalState,
|
||||
autoTunnel,
|
||||
)
|
||||
@@ -103,14 +103,13 @@ constructor(
|
||||
|
||||
private suspend fun appReadyCheck() {
|
||||
val tunnelCount = appDataRepository.tunnels.count()
|
||||
uiState.takeWhile { it.tunnels.size != tunnelCount }.onCompletion {
|
||||
_isAppReady.emit(true)
|
||||
}.collect()
|
||||
uiState.first { it.tunnels.count() == tunnelCount }
|
||||
_isAppReady.emit(true)
|
||||
}
|
||||
|
||||
private suspend fun initTunnels() {
|
||||
tunnels.withData {
|
||||
it.filter { it.isActive }.forEach {
|
||||
tunnels.withData { tunnels ->
|
||||
tunnels.filter { it.isActive }.forEach {
|
||||
tunnelManager.startTunnel(it)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,8 +24,6 @@ import com.zaneschepke.wireguardautotunnel.util.extensions.toWgQuickString
|
||||
import com.zaneschepke.wireguardautotunnel.util.extensions.withData
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.amnezia.awg.config.Config
|
||||
@@ -44,15 +42,6 @@ constructor(
|
||||
appDataRepository: AppDataRepository,
|
||||
) : BaseViewModel(appDataRepository) {
|
||||
|
||||
private val _activeTunnels = MutableStateFlow<List<TunnelConf>>(emptyList())
|
||||
val activeTunnels = _activeTunnels.asStateFlow()
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
tunnelManager.activeTunnels().collect(_activeTunnels::emit)
|
||||
}
|
||||
}
|
||||
|
||||
fun onDelete(tunnel: TunnelConf) = viewModelScope.launch {
|
||||
appSettings.withData { settings ->
|
||||
tunnels.withData {
|
||||
|
||||
+10
-10
@@ -1,13 +1,13 @@
|
||||
[versions]
|
||||
accompanist = "0.37.0"
|
||||
activityCompose = "1.10.0"
|
||||
amneziawgAndroid = "1.2.6"
|
||||
accompanist = "0.37.2"
|
||||
activityCompose = "1.10.1"
|
||||
amneziawgAndroid = "1.3.0"
|
||||
androidx-junit = "1.2.1"
|
||||
appcompat = "1.7.0"
|
||||
biometricKtx = "1.2.0-alpha05"
|
||||
coreKtx = "1.15.0"
|
||||
datastorePreferences = "1.1.2"
|
||||
desugar_jdk_libs = "2.1.4"
|
||||
datastorePreferences = "1.1.3"
|
||||
desugar_jdk_libs = "2.1.5"
|
||||
espressoCore = "3.6.1"
|
||||
hiltAndroid = "2.55"
|
||||
hiltCompiler = "1.2.0"
|
||||
@@ -15,14 +15,14 @@ junit = "4.13.2"
|
||||
kotlinx-serialization-json = "1.8.0"
|
||||
lifecycle-runtime-compose = "2.8.7"
|
||||
material3 = "1.3.1"
|
||||
navigationCompose = "2.8.7"
|
||||
navigationCompose = "2.8.8"
|
||||
pinLockCompose = "1.0.4"
|
||||
roomVersion = "2.6.1"
|
||||
timber = "5.0.1"
|
||||
tunnel = "1.2.2"
|
||||
androidGradlePlugin = "8.8.0-alpha05"
|
||||
tunnel = "1.2.6"
|
||||
androidGradlePlugin = "8.10.0-alpha08"
|
||||
kotlin = "2.1.10"
|
||||
ksp = "2.1.10-1.0.30"
|
||||
ksp = "2.1.10-1.0.31"
|
||||
composeBom = "2025.02.00"
|
||||
compose = "1.7.8"
|
||||
workRuntimeKtxVersion = "2.10.0"
|
||||
@@ -32,7 +32,7 @@ gradlePlugins-grgit = "5.3.0"
|
||||
|
||||
#plugins
|
||||
material = "1.12.0"
|
||||
gradlePlugins-ktlint="12.1.2"
|
||||
gradlePlugins-ktlint="12.2.0"
|
||||
|
||||
|
||||
[libraries]
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,51 @@
|
||||
plugins {
|
||||
alias(libs.plugins.androidLibrary)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.zaneschepke.networkmonitor"
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 26
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
}
|
||||
create(Constants.PRERELEASE) {
|
||||
initWith(getByName(Constants.RELEASE))
|
||||
}
|
||||
|
||||
create(Constants.NIGHTLY) {
|
||||
initWith(getByName(Constants.RELEASE))
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = Constants.JVM_TARGET
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.material)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
|
||||
implementation(libs.tunnel)
|
||||
|
||||
implementation(libs.timber)
|
||||
}
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package com.zaneschepke.networkmonitor
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("com.zaneschepke.networkmonitor.test", appContext.packageName)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
>
|
||||
<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_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
</manifest>
|
||||
@@ -0,0 +1,151 @@
|
||||
package com.zaneschepke.networkmonitor
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkRequest
|
||||
import android.net.wifi.WifiManager
|
||||
import com.wireguard.android.util.RootShell
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import timber.log.Timber
|
||||
|
||||
class AndroidNetworkMonitor(
|
||||
context: Context,
|
||||
) : NetworkMonitor {
|
||||
|
||||
private val appContext = context.applicationContext
|
||||
private val connectivityManager = appContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
private val wifiManager = appContext.getSystemService(Context.WIFI_SERVICE) as WifiManager?
|
||||
private val rootShell = RootShell(context)
|
||||
|
||||
private var includeWifiSsid = false
|
||||
private var useRootShell = false
|
||||
|
||||
data class WifiState(val connected: Boolean = false, val ssid: String? = null)
|
||||
data class TransportState(val connected: Boolean = false)
|
||||
|
||||
private val wifiFlow: Flow<WifiState> = callbackFlow {
|
||||
var currentSsid: String? = null
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
fun getWifiSsid(): String? {
|
||||
if (!includeWifiSsid || wifiManager == null) return null
|
||||
return if (useRootShell) {
|
||||
rootShell.getCurrentWifiName()
|
||||
} else {
|
||||
wifiManager.connectionInfo?.ssid?.trim('"')?.takeIf {
|
||||
it != "<unknown>" && it.isNotEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val callback = object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
Timber.d("Wi-Fi onAvailable: network=$network")
|
||||
val capabilities = connectivityManager.getNetworkCapabilities(network)
|
||||
val connected = capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true
|
||||
if (connected) {
|
||||
val ssid = getWifiSsid()
|
||||
currentSsid = ssid
|
||||
trySend(WifiState(connected = true, ssid = ssid))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
Timber.d("Wi-Fi onLost: network=$network")
|
||||
currentSsid = null
|
||||
trySend(WifiState(connected = false, ssid = null))
|
||||
}
|
||||
|
||||
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
|
||||
val connected = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
val ssid = if (connected) getWifiSsid() else null
|
||||
if (ssid != currentSsid) {
|
||||
currentSsid = ssid
|
||||
trySend(WifiState(connected = connected, ssid = ssid))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val request = NetworkRequest.Builder()
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
.build()
|
||||
|
||||
connectivityManager.registerNetworkCallback(request, callback)
|
||||
trySend(WifiState())
|
||||
|
||||
awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
|
||||
}
|
||||
|
||||
private val cellularFlow: Flow<TransportState> = callbackFlow {
|
||||
val callback = object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
Timber.d("Cellular onAvailable: network=$network")
|
||||
trySend(TransportState(connected = true))
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
Timber.d("Cellular onLost: network=$network")
|
||||
trySend(TransportState(connected = false))
|
||||
}
|
||||
}
|
||||
|
||||
val request = NetworkRequest.Builder()
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||
.build()
|
||||
|
||||
connectivityManager.registerNetworkCallback(request, callback)
|
||||
trySend(TransportState())
|
||||
|
||||
awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
|
||||
}
|
||||
|
||||
private val ethernetFlow: Flow<TransportState> = callbackFlow {
|
||||
val callback = object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
Timber.d("Ethernet onAvailable: network=$network")
|
||||
trySend(TransportState(connected = true))
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
Timber.d("Ethernet onLost: network=$network")
|
||||
trySend(TransportState(connected = false))
|
||||
}
|
||||
}
|
||||
|
||||
val request = NetworkRequest.Builder()
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
.addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
|
||||
.build()
|
||||
|
||||
connectivityManager.registerNetworkCallback(request, callback)
|
||||
trySend(TransportState())
|
||||
|
||||
awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
|
||||
}
|
||||
|
||||
override fun getNetworkStatusFlow(includeWifiSsid: Boolean, useRootShell: Boolean): Flow<NetworkStatus> {
|
||||
this.includeWifiSsid = includeWifiSsid
|
||||
this.useRootShell = useRootShell
|
||||
return combine(wifiFlow, cellularFlow, ethernetFlow) { wifi, cellular, ethernet ->
|
||||
val hasAnyConnection = wifi.connected || cellular.connected || ethernet.connected
|
||||
if (hasAnyConnection) {
|
||||
NetworkStatus.Connected(
|
||||
wifiSsid = wifi.ssid,
|
||||
wifiConnected = wifi.connected,
|
||||
cellularConnected = cellular.connected,
|
||||
ethernetConnected = ethernet.connected,
|
||||
)
|
||||
} else {
|
||||
NetworkStatus.Disconnected
|
||||
}.also { Timber.d("NetworkStatus: $it") }
|
||||
}.distinctUntilChanged()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.zaneschepke.networkmonitor
|
||||
|
||||
import com.wireguard.android.util.RootShell
|
||||
|
||||
fun RootShell.getCurrentWifiName(): String? {
|
||||
val response = mutableListOf<String>()
|
||||
this.run(response, "dumpsys wifi | grep 'Supplicant state: COMPLETED' | grep -o 'SSID: [^,]*' | cut -d ' ' -f2- | tr -d '\"'")
|
||||
return response.firstOrNull()
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.zaneschepke.networkmonitor
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface NetworkMonitor {
|
||||
fun getNetworkStatusFlow(includeWifiSsid: Boolean, useRootShell: Boolean): Flow<NetworkStatus>
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.zaneschepke.networkmonitor
|
||||
|
||||
sealed class NetworkStatus {
|
||||
data object Disconnected : NetworkStatus() {
|
||||
override val wifiConnected = false
|
||||
override val ethernetConnected = false
|
||||
override val cellularConnected = false
|
||||
}
|
||||
|
||||
data class Connected(
|
||||
val wifiSsid: String? = null,
|
||||
override val wifiConnected: Boolean = false,
|
||||
override val ethernetConnected: Boolean = false,
|
||||
override val cellularConnected: Boolean = false,
|
||||
) : NetworkStatus()
|
||||
|
||||
abstract val wifiConnected: Boolean
|
||||
abstract val ethernetConnected: Boolean
|
||||
abstract val cellularConnected: Boolean
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.zaneschepke.networkmonitor
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
||||
@@ -34,3 +34,4 @@ rootProject.name = "WG Tunnel"
|
||||
|
||||
include(":app")
|
||||
include(":logcatter")
|
||||
include(":networkmonitor")
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
1
|
||||
4
|
||||
Reference in New Issue
Block a user