diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index d9d7bbf9..472d989d 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -144,8 +144,8 @@ android {
}
dependencies {
-
implementation(project(":logcatter"))
+ implementation(project(":networkmonitor"))
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index abd9b7f4..d0ac2442 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,13 +3,8 @@
xmlns:tools="http://schemas.android.com/tools">
-
-
-
-
-
-
-
+
+
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('"')
- }
- }
-}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/network/NetworkExtensions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/network/NetworkExtensions.kt
deleted file mode 100644
index b973287b..00000000
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/network/NetworkExtensions.kt
+++ /dev/null
@@ -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
-}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/network/NetworkMonitor.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/network/NetworkMonitor.kt
deleted file mode 100644
index e33f75de..00000000
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/network/NetworkMonitor.kt
+++ /dev/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
-
- // util to help limit location queries
- val didWifiChangeSinceLastCapabilitiesQuery: Boolean
- fun getWifiCapabilities(): NetworkCapabilities?
-}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/service/autotunnel/AutoTunnelService.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/service/autotunnel/AutoTunnelService.kt
index 9731f3d6..54934c98 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/service/autotunnel/AutoTunnelService.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/service/autotunnel/AutoTunnelService.kt
@@ -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
-
@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> {
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)
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/BaseTunnel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/BaseTunnel.kt
index b9ba1f10..354394ac 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/BaseTunnel.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/BaseTunnel.kt
@@ -1,8 +1,9 @@
package com.zaneschepke.wireguardautotunnel.core.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
@@ -28,7 +29,7 @@ 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
@@ -159,9 +160,10 @@ 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 startStateJob(tunnel: TunnelConf) {
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/KernelTunnel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/KernelTunnel.kt
index 6b931632..5c9ac87b 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/KernelTunnel.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/KernelTunnel.kt
@@ -3,7 +3,7 @@ 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
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/UserspaceTunnel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/UserspaceTunnel.kt
index 22367f26..2caa56bb 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/UserspaceTunnel.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/core/tunnel/UserspaceTunnel.kt
@@ -1,7 +1,7 @@
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
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/di/ServiceModule.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/di/ServiceModule.kt
deleted file mode 100644
index 31134fa3..00000000
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/di/ServiceModule.kt
+++ /dev/null
@@ -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
-}
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/di/TunnelModule.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/di/TunnelModule.kt
index 8f1677e3..e1853ece 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/di/TunnelModule.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/di/TunnelModule.kt
@@ -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,6 +99,12 @@ class TunnelModule {
return TunnelManager(kernelTunnel, userspaceTunnel, appDataRepository, applicationScope, ioDispatcher)
}
+ @Provides
+ @Singleton
+ fun provideNetworkMonitor(@ApplicationContext context: Context): NetworkMonitor {
+ return AndroidNetworkMonitor(context)
+ }
+
@Singleton
@Provides
fun provideServiceManager(
diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/TunnelExtensions.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/TunnelExtensions.kt
index 6b5b657f..243b6392 100644
--- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/TunnelExtensions.kt
+++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/util/extensions/TunnelExtensions.kt
@@ -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
@@ -87,12 +86,6 @@ fun Config.toWgQuickString(): String {
return lines.joinToString(System.lineSeparator())
}
-fun RootShell.getCurrentWifiName(): String? {
- val response = mutableListOf()
- 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)
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 209a24f2..05c2581d 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,13 +1,13 @@
[versions]
-accompanist = "0.37.0"
-activityCompose = "1.10.0"
+accompanist = "0.37.2"
+activityCompose = "1.10.1"
amneziawgAndroid = "1.2.9"
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,7 +15,7 @@ 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"
diff --git a/networkmonitor/.gitignore b/networkmonitor/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/networkmonitor/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/networkmonitor/build.gradle.kts b/networkmonitor/build.gradle.kts
new file mode 100644
index 00000000..0c14c1d7
--- /dev/null
+++ b/networkmonitor/build.gradle.kts
@@ -0,0 +1,44 @@
+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")
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+ kotlinOptions {
+ jvmTarget = "11"
+ }
+}
+
+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)
+}
diff --git a/networkmonitor/consumer-rules.pro b/networkmonitor/consumer-rules.pro
new file mode 100644
index 00000000..e69de29b
diff --git a/networkmonitor/proguard-rules.pro b/networkmonitor/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/networkmonitor/proguard-rules.pro
@@ -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
\ No newline at end of file
diff --git a/networkmonitor/src/androidTest/java/com/zaneschepke/networkmonitor/ExampleInstrumentedTest.kt b/networkmonitor/src/androidTest/java/com/zaneschepke/networkmonitor/ExampleInstrumentedTest.kt
new file mode 100644
index 00000000..c58241f6
--- /dev/null
+++ b/networkmonitor/src/androidTest/java/com/zaneschepke/networkmonitor/ExampleInstrumentedTest.kt
@@ -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)
+ }
+}
diff --git a/networkmonitor/src/main/AndroidManifest.xml b/networkmonitor/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..1fe5bbd8
--- /dev/null
+++ b/networkmonitor/src/main/AndroidManifest.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
diff --git a/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/AndroidNetworkMonitor.kt b/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/AndroidNetworkMonitor.kt
new file mode 100644
index 00000000..2b01bcbb
--- /dev/null
+++ b/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/AndroidNetworkMonitor.kt
@@ -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 = 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 != "" && 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 = 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 = 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 {
+ 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()
+ }
+}
diff --git a/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/Extensions.kt b/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/Extensions.kt
new file mode 100644
index 00000000..c735dce0
--- /dev/null
+++ b/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/Extensions.kt
@@ -0,0 +1,9 @@
+package com.zaneschepke.networkmonitor
+
+import com.wireguard.android.util.RootShell
+
+fun RootShell.getCurrentWifiName(): String? {
+ val response = mutableListOf()
+ this.run(response, "dumpsys wifi | grep 'Supplicant state: COMPLETED' | grep -o 'SSID: [^,]*' | cut -d ' ' -f2- | tr -d '\"'")
+ return response.firstOrNull()
+}
diff --git a/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/NetworkMonitor.kt b/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/NetworkMonitor.kt
new file mode 100644
index 00000000..f5c6bac0
--- /dev/null
+++ b/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/NetworkMonitor.kt
@@ -0,0 +1,7 @@
+package com.zaneschepke.networkmonitor
+
+import kotlinx.coroutines.flow.Flow
+
+interface NetworkMonitor {
+ fun getNetworkStatusFlow(includeWifiSsid: Boolean, useRootShell: Boolean): Flow
+}
diff --git a/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/NetworkStatus.kt b/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/NetworkStatus.kt
new file mode 100644
index 00000000..3d03613b
--- /dev/null
+++ b/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/NetworkStatus.kt
@@ -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
+}
diff --git a/networkmonitor/src/test/java/com/zaneschepke/networkmonitor/ExampleUnitTest.kt b/networkmonitor/src/test/java/com/zaneschepke/networkmonitor/ExampleUnitTest.kt
new file mode 100644
index 00000000..a5967b52
--- /dev/null
+++ b/networkmonitor/src/test/java/com/zaneschepke/networkmonitor/ExampleUnitTest.kt
@@ -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)
+ }
+}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 15c4e847..f412dd14 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -34,3 +34,4 @@ rootProject.name = "WG Tunnel"
include(":app")
include(":logcatter")
+include(":networkmonitor")