From 3ea4aea5cfe9db47b9030508c460528dae4f93bf Mon Sep 17 00:00:00 2001 From: Zane Schepke Date: Sat, 1 Mar 2025 20:17:19 -0500 Subject: [PATCH] fix: improve network status monitoring --- app/build.gradle.kts | 2 +- app/src/main/AndroidManifest.xml | 9 +- .../network/InternetConnectivityMonitor.kt | 161 ------------------ .../core/network/NetworkExtensions.kt | 16 -- .../core/network/NetworkMonitor.kt | 13 -- .../service/autotunnel/AutoTunnelService.kt | 85 +++++---- .../core/tunnel/BaseTunnel.kt | 12 +- .../core/tunnel/KernelTunnel.kt | 2 +- .../core/tunnel/UserspaceTunnel.kt | 2 +- .../wireguardautotunnel/di/ServiceModule.kt | 16 -- .../wireguardautotunnel/di/TunnelModule.kt | 9 +- .../util/extensions/TunnelExtensions.kt | 7 - gradle/libs.versions.toml | 10 +- networkmonitor/.gitignore | 1 + networkmonitor/build.gradle.kts | 44 +++++ networkmonitor/consumer-rules.pro | 0 networkmonitor/proguard-rules.pro | 21 +++ .../networkmonitor/ExampleInstrumentedTest.kt | 24 +++ networkmonitor/src/main/AndroidManifest.xml | 9 + .../networkmonitor/AndroidNetworkMonitor.kt | 151 ++++++++++++++++ .../zaneschepke/networkmonitor/Extensions.kt | 9 + .../networkmonitor/NetworkMonitor.kt | 7 + .../networkmonitor/NetworkStatus.kt | 20 +++ .../networkmonitor/ExampleUnitTest.kt | 17 ++ settings.gradle.kts | 1 + 25 files changed, 371 insertions(+), 277 deletions(-) delete mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/core/network/InternetConnectivityMonitor.kt delete mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/core/network/NetworkExtensions.kt delete mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/core/network/NetworkMonitor.kt delete mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/di/ServiceModule.kt create mode 100644 networkmonitor/.gitignore create mode 100644 networkmonitor/build.gradle.kts create mode 100644 networkmonitor/consumer-rules.pro create mode 100644 networkmonitor/proguard-rules.pro create mode 100644 networkmonitor/src/androidTest/java/com/zaneschepke/networkmonitor/ExampleInstrumentedTest.kt create mode 100644 networkmonitor/src/main/AndroidManifest.xml create mode 100644 networkmonitor/src/main/java/com/zaneschepke/networkmonitor/AndroidNetworkMonitor.kt create mode 100644 networkmonitor/src/main/java/com/zaneschepke/networkmonitor/Extensions.kt create mode 100644 networkmonitor/src/main/java/com/zaneschepke/networkmonitor/NetworkMonitor.kt create mode 100644 networkmonitor/src/main/java/com/zaneschepke/networkmonitor/NetworkStatus.kt create mode 100644 networkmonitor/src/test/java/com/zaneschepke/networkmonitor/ExampleUnitTest.kt 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")