From 4beeb4e01ec9a7a803583c81f769c482fe9cf281 Mon Sep 17 00:00:00 2001 From: Zane Schepke Date: Tue, 4 Nov 2025 17:48:40 -0500 Subject: [PATCH] fix: network monitoring bug --- .../service/autotunnel/AutoTunnelService.kt | 11 +- .../domain/state/AutoTunnelState.kt | 4 +- .../networkmonitor/AndroidNetworkMonitor.kt | 549 +++++++++--------- .../networkmonitor/ConnectivityState.kt | 11 +- .../networkmonitor/util/Extensions.kt | 31 + 5 files changed, 335 insertions(+), 271 deletions(-) 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 ea30ab6a..61a2c67c 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 @@ -61,6 +61,9 @@ class AutoTunnelService : LifecycleService() { private val autoTunnelStateFlow = MutableStateFlow(defaultState) + private var autoTunnelJob: Job? = null + private var permissionsJob: Job? = null + class LocalBinder(service: AutoTunnelService) : Binder() { private val serviceRef = WeakReference(service) @@ -89,8 +92,10 @@ class AutoTunnelService : LifecycleService() { fun start() { launchWatcherNotification() - startAutoTunnelStateJob() - startLocationPermissionsNotificationJob() + autoTunnelJob?.cancel() + autoTunnelJob = startAutoTunnelStateJob() + permissionsJob?.cancel() + permissionsJob = startLocationPermissionsNotificationJob() } fun stop() { @@ -130,7 +135,7 @@ class AutoTunnelService : LifecycleService() { ) } - private fun startAutoTunnelStateJob() = + private fun startAutoTunnelStateJob(): Job = lifecycleScope.launch(ioDispatcher) { val networkFlow = debouncedConnectivityStateFlow diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/state/AutoTunnelState.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/state/AutoTunnelState.kt index c297794c..62e2292e 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/state/AutoTunnelState.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/domain/state/AutoTunnelState.kt @@ -59,8 +59,8 @@ data class AutoTunnelState( return DoNothing } - private val ethernetActive: Boolean = networkState.activeNetwork is ActiveNetwork.Cellular - private val mobileDataActive: Boolean = networkState.activeNetwork is ActiveNetwork.Ethernet + private val ethernetActive: Boolean = networkState.activeNetwork is ActiveNetwork.Ethernet + private val mobileDataActive: Boolean = networkState.activeNetwork is ActiveNetwork.Cellular private val wifiActive: Boolean = networkState.activeNetwork is ActiveNetwork.Wifi private fun preferredMobileDataTunnel(): TunnelConfig? { diff --git a/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/AndroidNetworkMonitor.kt b/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/AndroidNetworkMonitor.kt index bcbabe14..e1269864 100644 --- a/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/AndroidNetworkMonitor.kt +++ b/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/AndroidNetworkMonitor.kt @@ -1,25 +1,20 @@ package com.zaneschepke.networkmonitor -import android.Manifest import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter -import android.content.pm.PackageManager import android.location.LocationManager import android.net.ConnectivityManager -import android.net.ConnectivityManager.NetworkCallback.FLAG_INCLUDE_LOCATION_INFO import android.net.Network import android.net.NetworkCapabilities import android.net.NetworkRequest import android.net.wifi.WifiManager import android.os.Build -import androidx.core.content.ContextCompat import com.wireguard.android.util.RootShell import com.zaneschepke.networkmonitor.AndroidNetworkMonitor.WifiDetectionMethod.* import com.zaneschepke.networkmonitor.shizuku.ShizukuShell import com.zaneschepke.networkmonitor.util.* -import java.util.concurrent.ConcurrentHashMap import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.awaitClose @@ -68,119 +63,103 @@ class AndroidNetworkMonitor( private val locationManager = appContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager? - private val activeWifiNetworks = - ConcurrentHashMap>() - private val permissionsChangedFlow = MutableStateFlow(false) private var permissionReceiver: BroadcastReceiver? = null private var locationServicesReceiver: BroadcastReceiver? = null private var defaultNetworkCallback: ConnectivityManager.NetworkCallback? = null - private var wifiInterfaceCallback: ConnectivityManager.NetworkCallback? = null - private var cellularInterfaceCallback: ConnectivityManager.NetworkCallback? = null - - private val isAirplaneModeOn: Boolean - get() = - android.provider.Settings.Global.getInt( - appContext.contentResolver, - android.provider.Settings.Global.AIRPLANE_MODE_ON, - 0, - ) != 0 + private var wifiCallback: ConnectivityManager.NetworkCallback? = null + private var cellularCallback: ConnectivityManager.NetworkCallback? = null + private var ethernetCallback: ConnectivityManager.NetworkCallback? = null @OptIn(ExperimentalCoroutinesApi::class) private val defaultNetworkFlow: Flow = - combine(configurationListener.detectionMethod, permissionsChangedFlow) { - detectionMethod, - changed -> - Pair(detectionMethod, changed) - } - .flatMapLatest { (detectionMethod, _) -> - createDefaultNetworkCallbackFlow(detectionMethod) + combine(configurationListener.detectionMethod, permissionsChangedFlow) { detectionMethod, _ + -> + detectionMethod } + .flatMapLatest { detectionMethod -> + callbackFlow { + if ( + Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && detectionMethod == DEFAULT + ) { + defaultNetworkCallback = + object : + ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) { + override fun onAvailable(network: Network) { + Timber.d("Default onAvailable: $network") + } - private fun isAndroidTv(): Boolean = - appContext.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK) + override fun onLost(network: Network) { + trySend(TransportEvent.Lost(network)) + } - private fun hasRequiredLocationPermissions(): Boolean { - val fineLocationGranted = - ContextCompat.checkSelfPermission( - appContext, - Manifest.permission.ACCESS_FINE_LOCATION, - ) == PackageManager.PERMISSION_GRANTED - val backgroundLocationGranted = - if ( - (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) && - // exclude Android TV on Q as background location is not required on this - // version - !(Build.VERSION.SDK_INT == Build.VERSION_CODES.Q && isAndroidTv()) - ) { - ContextCompat.checkSelfPermission( - appContext, - Manifest.permission.ACCESS_BACKGROUND_LOCATION, - ) == PackageManager.PERMISSION_GRANTED - } else { - true // No need for ACCESS_BACKGROUND_LOCATION on Android P or Android TV on Q - } - return fineLocationGranted && backgroundLocationGranted - } - - private fun createDefaultNetworkCallbackFlow( - detectionMethod: WifiDetectionMethod - ): Flow = callbackFlow { - val onAvailable: (Network) -> Unit = { network -> - Timber.d("Network onAvailable: network=$network") - } - - val onLost: (Network) -> Unit = { network -> - Timber.d("Network onLost: network=$network") - trySend(TransportEvent.Lost(network)) - } - - val onCapabilitiesChanged: (Network, NetworkCapabilities) -> Unit = - { network, networkCapabilities -> - val isValidated = - networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) - val hasInternet = - networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) - - Timber.d("onCapabilitiesChanged: network=$network, validated: $isValidated") - - if (isValidated && hasInternet) { - val event = - when { - networkCapabilities.hasTransport( - NetworkCapabilities.TRANSPORT_WIFI - ) -> { - activeWifiNetworks[network.toString()] = - Pair(network, networkCapabilities) - TransportEvent.CapabilitiesChanged( - network, - networkCapabilities, - detectionMethod, - ) + override fun onCapabilitiesChanged( + network: Network, + caps: NetworkCapabilities, + ) { + trySend(TransportEvent.CapabilitiesChanged(network, caps)) + } } - networkCapabilities.hasTransport( - NetworkCapabilities.TRANSPORT_CELLULAR - ) -> { - activeWifiNetworks.clear() - TransportEvent.CapabilitiesChanged(network, networkCapabilities) + } else { + defaultNetworkCallback = + object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + Timber.d("Default onAvailable: $network") + } + + override fun onLost(network: Network) { + trySend(TransportEvent.Lost(network)) + } + + override fun onCapabilitiesChanged( + network: Network, + caps: NetworkCapabilities, + ) { + trySend(TransportEvent.CapabilitiesChanged(network, caps)) + } } - networkCapabilities.hasTransport( - NetworkCapabilities.TRANSPORT_ETHERNET - ) -> { - activeWifiNetworks.clear() - TransportEvent.CapabilitiesChanged(network, networkCapabilities) - } - else -> TransportEvent.Unknown - } - trySend(event) - } else { - activeWifiNetworks.remove(network.toString()) - trySend(TransportEvent.Lost(network)) + } + connectivityManager?.registerDefaultNetworkCallback(defaultNetworkCallback!!) + + trySend( + TransportEvent.Permissions( + Permissions( + locationManager?.isLocationServicesEnabled() ?: false, + appContext.hasRequiredLocationPermissions(), + ) + ) + ) + + awaitClose { + connectivityManager?.unregisterNetworkCallback(defaultNetworkCallback!!) + } } } - val callback: ConnectivityManager.NetworkCallback = + @OptIn(ExperimentalCoroutinesApi::class) + private val wifiFlow: Flow = + combine(configurationListener.detectionMethod, permissionsChangedFlow) { detectionMethod, _ + -> + detectionMethod + } + .flatMapLatest { detectionMethod -> createWifiNetworkCallbackFlow(detectionMethod) } + + private fun createWifiNetworkCallbackFlow( + detectionMethod: WifiDetectionMethod + ): Flow = callbackFlow { + val onAvailable: (Network) -> Unit = { network -> Timber.d("WiFi onAvailable: $network") } + val onLost: (Network) -> Unit = { network -> + Timber.d("WiFi onLost: $network") + trySend(TransportEvent.Lost(network)) + } + val onCapabilitiesChanged: (Network, NetworkCapabilities) -> Unit = { network, caps -> + if (caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { + trySend(TransportEvent.CapabilitiesChanged(network, caps)) + } + } + + wifiCallback = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && detectionMethod == DEFAULT) { object : ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) { override fun onAvailable(network: Network) = onAvailable(network) @@ -189,8 +168,8 @@ class AndroidNetworkMonitor( override fun onCapabilitiesChanged( network: Network, - networkCapabilities: NetworkCapabilities, - ) = onCapabilitiesChanged(network, networkCapabilities) + caps: NetworkCapabilities, + ) = onCapabilitiesChanged(network, caps) } } else { object : ConnectivityManager.NetworkCallback() { @@ -200,94 +179,92 @@ class AndroidNetworkMonitor( override fun onCapabilitiesChanged( network: Network, - networkCapabilities: NetworkCapabilities, - ) = onCapabilitiesChanged(network, networkCapabilities) + caps: NetworkCapabilities, + ) = onCapabilitiesChanged(network, caps) } } - defaultNetworkCallback = callback - - connectivityManager?.registerDefaultNetworkCallback(defaultNetworkCallback!!) - - trySend( - TransportEvent.Permissions( - permissions = - Permissions( - locationManager?.isLocationServicesEnabled() ?: false, - hasRequiredLocationPermissions(), - ) - ) - ) - - awaitClose { - runCatching { connectivityManager?.unregisterNetworkCallback(defaultNetworkCallback!!) } - .onFailure { Timber.e(it, "Error unregistering default network callback") } - } - } - - private val wifiInterfaceFlow: Flow = callbackFlow { - val localCallback = - object : ConnectivityManager.NetworkCallback() { - override fun onAvailable(network: Network) { - Timber.d("Wi-Fi onAvailable: network=$network") - trySend(true) - } - - override fun onLost(network: Network) { - Timber.d("Wi-Fi onLost: network=$network") - trySend(false) - } - } - wifiInterfaceCallback = localCallback val request = NetworkRequest.Builder().addTransportType(NetworkCapabilities.TRANSPORT_WIFI).build() - - connectivityManager?.registerNetworkCallback(request, wifiInterfaceCallback!!) - - @Suppress("DEPRECATION") val isWifiInitiallyOn = wifiManager?.isWifiEnabled == true - trySend(isWifiInitiallyOn) + connectivityManager?.registerNetworkCallback(request, wifiCallback!!) awaitClose { - runCatching { connectivityManager?.unregisterNetworkCallback(wifiInterfaceCallback!!) } - .onFailure { Timber.e(it, "Error unregistering Wi-Fi interface callback") } + runCatching { connectivityManager?.unregisterNetworkCallback(wifiCallback!!) } + .onFailure { Timber.e(it, "Error unregistering WiFi network callback") } } } - private val cellularInterfaceFlow: Flow = callbackFlow { - val localCallback = - object : ConnectivityManager.NetworkCallback() { - override fun onAvailable(network: Network) { - Timber.d("Cellular onAvailable: network=$network") - trySend(true) - } + private val cellularFlow: Flow = callbackFlow { + val onAvailable: (Network) -> Unit = { network -> + Timber.d("Cellular onAvailable: $network") + } + val onLost: (Network) -> Unit = { network -> + Timber.d("Cellular onLost: $network") + trySend(TransportEvent.Lost(network)) + } + val onCapabilitiesChanged: (Network, NetworkCapabilities) -> Unit = { network, caps -> + Timber.d("Cellular onCapabilitiesChanged: $network") + trySend(TransportEvent.CapabilitiesChanged(network, caps)) + } - override fun onLost(network: Network) { - Timber.d("Cellular onLost: network=$network") - trySend(false) - } + cellularCallback = + object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) = onAvailable(network) + + override fun onLost(network: Network) = onLost(network) + + override fun onCapabilitiesChanged(network: Network, caps: NetworkCapabilities) = + onCapabilitiesChanged(network, caps) } - cellularInterfaceCallback = localCallback val request = NetworkRequest.Builder() .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) .build() + connectivityManager?.registerNetworkCallback(request, cellularCallback!!) - connectivityManager?.registerNetworkCallback(request, cellularInterfaceCallback!!) - - // initial state - val initialCellularNetwork = connectivityManager?.activeNetwork - val initialCapabilities = - connectivityManager?.getNetworkCapabilities(initialCellularNetwork) - val isCellularInitiallyOn = - initialCapabilities?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true - trySend(isCellularInitiallyOn) + trySend(TransportEvent.Unknown) awaitClose { - runCatching { - connectivityManager?.unregisterNetworkCallback(cellularInterfaceCallback!!) - } - .onFailure { Timber.e(it, "Error unregistering cellular interface callback") } + runCatching { connectivityManager?.unregisterNetworkCallback(cellularCallback!!) } + .onFailure { Timber.e(it, "Error unregistering cellular network callback") } + } + } + + private val ethernetFlow: Flow = callbackFlow { + val onAvailable: (Network) -> Unit = { network -> + Timber.d("Ethernet onAvailable: $network") + } + val onLost: (Network) -> Unit = { network -> + Timber.d("Ethernet onLost: $network") + trySend(TransportEvent.Lost(network)) + } + val onCapabilitiesChanged: (Network, NetworkCapabilities) -> Unit = { network, caps -> + Timber.d("Ethernet onCapabilitiesChanged: $network") + trySend(TransportEvent.CapabilitiesChanged(network, caps)) + } + + ethernetCallback = + object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) = onAvailable(network) + + override fun onLost(network: Network) = onLost(network) + + override fun onCapabilitiesChanged(network: Network, caps: NetworkCapabilities) = + onCapabilitiesChanged(network, caps) + } + + val request = + NetworkRequest.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET) + .build() + connectivityManager?.registerNetworkCallback(request, ethernetCallback!!) + + trySend(TransportEvent.Unknown) + + awaitClose { + runCatching { connectivityManager?.unregisterNetworkCallback(ethernetCallback!!) } + .onFailure { Timber.e(it, "Error unregistering ethernet network callback") } } } @@ -297,7 +274,7 @@ class AndroidNetworkMonitor( override fun onReceive(context: Context, intent: Intent) { if (intent.action == Intent.ACTION_AIRPLANE_MODE_CHANGED) { Timber.d("Received airplane mode changed broadcast") - trySend(isAirplaneModeOn) + trySend(appContext.isAirplaneModeOn()) } } } @@ -306,7 +283,7 @@ class AndroidNetworkMonitor( appContext.registerReceiver(receiver, filter) // initial state - trySend(isAirplaneModeOn) + trySend(appContext.isAirplaneModeOn()) awaitClose { runCatching { appContext.unregisterReceiver(receiver) } @@ -314,6 +291,53 @@ class AndroidNetworkMonitor( } } + private val wifiStateFlow: Flow = + wifiFlow + .map { event -> + when (event) { + is TransportEvent.CapabilitiesChanged -> event.networkCapabilities + is TransportEvent.Lost -> null + else -> null + } + } + .stateIn(applicationScope, SharingStarted.Eagerly, null) + + private val cellularStateFlow: Flow = + cellularFlow + .map { event -> + when (event) { + is TransportEvent.CapabilitiesChanged -> + if ( + event.networkCapabilities.hasCapability( + NetworkCapabilities.NET_CAPABILITY_INTERNET + ) + ) + event.networkCapabilities + else null + is TransportEvent.Lost -> null + else -> null + } + } + .stateIn(applicationScope, SharingStarted.Eagerly, null) + + private val ethernetStateFlow: Flow = + ethernetFlow + .map { event -> + when (event) { + is TransportEvent.CapabilitiesChanged -> + if ( + event.networkCapabilities.hasCapability( + NetworkCapabilities.NET_CAPABILITY_INTERNET + ) + ) + event.networkCapabilities + else null + is TransportEvent.Lost -> null + else -> null + } + } + .stateIn(applicationScope, SharingStarted.Eagerly, null) + private suspend fun getSsidByDetectionMethod( detectionMethod: WifiDetectionMethod?, networkCapabilities: NetworkCapabilities?, @@ -345,90 +369,100 @@ class AndroidNetworkMonitor( .also { Timber.d("Current SSID via ${method.name}: $it") } } - override val connectivityStateFlow: SharedFlow = - combine( - defaultNetworkFlow.scan( - ConnectivityState( - activeNetwork = ActiveNetwork.Disconnected, - locationPermissionsGranted = hasRequiredLocationPermissions(), - locationServicesEnabled = - locationManager?.isLocationServicesEnabled() ?: false, - ) - ) { previous, event -> - when (event) { - is TransportEvent.CapabilitiesChanged -> { - when { - event.networkCapabilities.hasTransport( - NetworkCapabilities.TRANSPORT_WIFI - ) -> { - val ssid = - getSsidByDetectionMethod( - event.wifiDetectionMethod - ?: WifiDetectionMethod.DEFAULT, - event.networkCapabilities, - ) + private data class NetworkData( + val defaultEvent: TransportEvent, + val wifiCaps: NetworkCapabilities?, + val cellularCaps: NetworkCapabilities?, + val ethernetCaps: NetworkCapabilities?, + ) - previous.copy( - activeNetwork = - ActiveNetwork.Wifi( - ssid = ssid, - securityType = wifiManager?.getCurrentSecurityType(), - ) - ) + private val networkFlows: Flow = + combine(defaultNetworkFlow, wifiStateFlow, cellularStateFlow, ethernetStateFlow) { + defaultEvent, + wifiCaps, + cellularCaps, + ethernetCaps -> + NetworkData(defaultEvent, wifiCaps, cellularCaps, ethernetCaps) + } + + override val connectivityStateFlow: SharedFlow = + combine(networkFlows, airplaneModeFlow, configurationListener.detectionMethod) { + networkData, + isAirplaneOn, + detectionMethod -> + val defaultEvent = networkData.defaultEvent + val wifiCaps = networkData.wifiCaps + val cellularCaps = networkData.cellularCaps + val ethernetCaps = networkData.ethernetCaps + + val permissions = + when (defaultEvent) { + is TransportEvent.Permissions -> defaultEvent.permissions + else -> + Permissions( + locationManager?.isLocationServicesEnabled() ?: false, + appContext.hasRequiredLocationPermissions(), + ) + } + + val defaultCaps = + when (defaultEvent) { + is TransportEvent.CapabilitiesChanged -> defaultEvent.networkCapabilities + else -> + connectivityManager?.getNetworkCapabilities( + connectivityManager.activeNetwork + ) + } + ?: return@combine ConnectivityState( + ActiveNetwork.Disconnected, + permissions.locationServicesEnabled, + permissions.locationPermissionGranted, + ) + + val isValidated = + defaultCaps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + val hasInternet = + defaultCaps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + + if (!isValidated || !hasInternet) { + return@combine ConnectivityState( + ActiveNetwork.Disconnected, + permissions.locationServicesEnabled, + permissions.locationPermissionGranted, + ) + } else { + val activeNetwork: ActiveNetwork = + if (defaultCaps.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) { + // Ignore VPN, determine underlying + when { + wifiCaps != null -> { + val ssid = getSsidByDetectionMethod(detectionMethod, wifiCaps) + ActiveNetwork.Wifi(ssid, wifiManager?.getCurrentSecurityType()) } - event.networkCapabilities.hasTransport( - NetworkCapabilities.TRANSPORT_CELLULAR - ) -> { - activeWifiNetworks.clear() - previous.copy(activeNetwork = ActiveNetwork.Cellular) + ethernetCaps != null -> ActiveNetwork.Ethernet + cellularCaps != null && !isAirplaneOn -> ActiveNetwork.Cellular + else -> ActiveNetwork.Disconnected + } + } else { + when { + defaultCaps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> { + val ssid = + getSsidByDetectionMethod(detectionMethod, defaultCaps) + ActiveNetwork.Wifi(ssid, wifiManager?.getCurrentSecurityType()) } - event.networkCapabilities.hasTransport( - NetworkCapabilities.TRANSPORT_ETHERNET - ) -> { - activeWifiNetworks.clear() - previous.copy(activeNetwork = ActiveNetwork.Ethernet) - } - else -> previous + defaultCaps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) && + !isAirplaneOn -> ActiveNetwork.Cellular + defaultCaps.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> + ActiveNetwork.Ethernet + else -> ActiveNetwork.Disconnected } } - is TransportEvent.Lost -> - previous.copy(activeNetwork = ActiveNetwork.Disconnected) - is TransportEvent.Permissions -> { - previous.copy( - locationPermissionsGranted = - event.permissions.locationPermissionGranted, - locationServicesEnabled = event.permissions.locationServicesEnabled, - ) - } - is TransportEvent.Available -> previous - is TransportEvent.Unknown -> previous - } - }, - wifiInterfaceFlow, - airplaneModeFlow, - cellularInterfaceFlow, - ) { defaultState, isWifiInterfaceOn, isAirplaneModeOn, isCellularInterfaceOn -> - val activeNetwork = - when { - // Wi-Fi interface disabled, force disconnected - !isWifiInterfaceOn && defaultState.activeNetwork is ActiveNetwork.Wifi -> - ActiveNetwork.Disconnected - // Cellular active when airplane mode on - isAirplaneModeOn && defaultState.activeNetwork is ActiveNetwork.Cellular -> - ActiveNetwork.Disconnected - // Cellular active when cellular interface disabled - !isCellularInterfaceOn && - defaultState.activeNetwork is ActiveNetwork.Cellular -> - ActiveNetwork.Disconnected - else -> defaultState.activeNetwork - } - - ConnectivityState( - activeNetwork = activeNetwork, - locationPermissionsGranted = defaultState.locationPermissionsGranted, - locationServicesEnabled = defaultState.locationServicesEnabled, + ConnectivityState( + activeNetwork, + permissions.locationServicesEnabled, + permissions.locationPermissionGranted, ) - .also { Timber.i("Connectivity Status: $it") } + } } .distinctUntilChanged() .shareIn(applicationScope, SharingStarted.Eagerly, replay = 1) @@ -452,7 +486,7 @@ class AndroidNetworkMonitor( object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { if (intent.action == actionPermissionCheck) { - val isGranted = hasRequiredLocationPermissions() + val isGranted = appContext.hasRequiredLocationPermissions() Timber.d("Received permission check broadcast, isGranted: $isGranted") if ( connectivityStateFlow.replayCache @@ -462,7 +496,6 @@ class AndroidNetworkMonitor( Timber.d( "Location permissions have changed, canceling and restarting callback flow" ) - activeWifiNetworks.clear() permissionsChangedFlow.update { !permissionsChangedFlow.value } } } @@ -487,7 +520,6 @@ class AndroidNetworkMonitor( Timber.d( "Location services have changed, canceling and restarting callback flow" ) - activeWifiNetworks.clear() permissionsChangedFlow.update { !permissionsChangedFlow.value } } } @@ -508,10 +540,9 @@ class AndroidNetworkMonitor( locationServicesReceiver?.let { appContext.unregisterReceiver(it) } defaultNetworkCallback?.let { connectivityManager?.unregisterNetworkCallback(it) } - wifiInterfaceCallback?.let { connectivityManager?.unregisterNetworkCallback(it) } - cellularInterfaceCallback?.let { - connectivityManager?.unregisterNetworkCallback(it) - } + wifiCallback?.let { connectivityManager?.unregisterNetworkCallback(it) } + cellularCallback?.let { connectivityManager?.unregisterNetworkCallback(it) } + ethernetCallback?.let { connectivityManager?.unregisterNetworkCallback(it) } } .onFailure { Timber.e(it, "Error during cleanup") } Timber.d("NetworkMonitor cleaned up") diff --git a/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/ConnectivityState.kt b/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/ConnectivityState.kt index 47fc9d40..6a5d8297 100644 --- a/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/ConnectivityState.kt +++ b/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/ConnectivityState.kt @@ -27,17 +27,14 @@ data class ConnectivityState( } } +data class Permissions(val locationServicesEnabled: Boolean, val locationPermissionGranted: Boolean) + sealed class ActiveNetwork { data object Disconnected : ActiveNetwork() - data object Ethernet : ActiveNetwork() + data class Wifi(val ssid: String, val securityType: WifiSecurityType?) : ActiveNetwork() data object Cellular : ActiveNetwork() - data class Wifi(val ssid: String, val securityType: WifiSecurityType? = null) : ActiveNetwork() + data object Ethernet : ActiveNetwork() } - -data class Permissions( - val locationServicesEnabled: Boolean = false, - val locationPermissionGranted: Boolean = false, -) diff --git a/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/util/Extensions.kt b/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/util/Extensions.kt index 1b7278aa..d0d89d31 100644 --- a/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/util/Extensions.kt +++ b/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/util/Extensions.kt @@ -1,10 +1,15 @@ package com.zaneschepke.networkmonitor.util +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager import android.location.LocationManager import android.net.NetworkCapabilities import android.net.wifi.WifiInfo import android.net.wifi.WifiManager import android.os.Build +import android.provider.Settings +import androidx.core.content.ContextCompat import com.wireguard.android.util.RootShell import com.zaneschepke.networkmonitor.AndroidNetworkMonitor.Companion.ANDROID_UNKNOWN_SSID import kotlinx.coroutines.Dispatchers @@ -62,3 +67,29 @@ fun LocationManager.isLocationServicesEnabled(): Boolean { false } } + +fun Context.hasRequiredLocationPermissions(): Boolean { + val fineLocationGranted = + ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == + PackageManager.PERMISSION_GRANTED + val backgroundLocationGranted = + if ( + (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) && + // exclude Android TV on Q as background location is not required on this + // version + !(Build.VERSION.SDK_INT == Build.VERSION_CODES.Q && + packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) + ) { + ContextCompat.checkSelfPermission( + this, + Manifest.permission.ACCESS_BACKGROUND_LOCATION, + ) == PackageManager.PERMISSION_GRANTED + } else { + true // No need for ACCESS_BACKGROUND_LOCATION on Android P or Android TV on Q + } + return fineLocationGranted && backgroundLocationGranted +} + +fun Context.isAirplaneModeOn(): Boolean { + return Settings.Global.getInt(contentResolver, Settings.Global.AIRPLANE_MODE_ON, 0) != 0 +}