fix: network detection bug

closes #1032
This commit is contained in:
Zane Schepke
2025-11-03 08:20:35 -05:00
parent df864ade95
commit f61e6d6c6e
10 changed files with 337 additions and 403 deletions
@@ -24,7 +24,7 @@ import com.zaneschepke.wireguardautotunnel.domain.repository.AutoTunnelSettingsR
import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository import com.zaneschepke.wireguardautotunnel.domain.repository.GeneralSettingRepository
import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository import com.zaneschepke.wireguardautotunnel.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.domain.state.AutoTunnelState import com.zaneschepke.wireguardautotunnel.domain.state.AutoTunnelState
import com.zaneschepke.wireguardautotunnel.domain.state.NetworkState import com.zaneschepke.wireguardautotunnel.domain.state.toDomain
import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.to import com.zaneschepke.wireguardautotunnel.util.extensions.to
import com.zaneschepke.wireguardautotunnel.util.extensions.toMillis import com.zaneschepke.wireguardautotunnel.util.extensions.toMillis
@@ -129,7 +129,7 @@ class AutoTunnelService : LifecycleService() {
val networkFlow = val networkFlow =
debouncedConnectivityStateFlow debouncedConnectivityStateFlow
.flowOn(ioDispatcher) .flowOn(ioDispatcher)
.map(NetworkState::from) .map { it.toDomain() }
.map(::NetworkChange) .map(::NetworkChange)
.distinctUntilChanged() .distinctUntilChanged()
@@ -266,8 +266,8 @@ class AutoTunnelService : LifecycleService() {
.map { .map {
NetworkPermissionState( NetworkPermissionState(
it.settings.wifiDetectionMethod.to(), it.settings.wifiDetectionMethod.to(),
it.networkState.locationServicesEnabled == true, it.networkState.locationServicesEnabled,
it.networkState.locationPermissionGranted == true, it.networkState.locationPermissionGranted,
(it.tunnels.any { tunnel -> tunnel.tunnelNetworks.isNotEmpty() } || (it.tunnels.any { tunnel -> tunnel.tunnelNetworks.isNotEmpty() } ||
it.settings.trustedNetworkSSIDs.isNotEmpty()), it.settings.trustedNetworkSSIDs.isNotEmpty()),
) )
@@ -95,26 +95,7 @@ constructor(
val connectivityStateFlow = networkMonitor.connectivityStateFlow.stateIn(this) val connectivityStateFlow = networkMonitor.connectivityStateFlow.stateIn(this)
val isNetworkConnected = connectivityStateFlow.map { it.hasConnectivity() }.stateIn(this) val isNetworkConnected = connectivityStateFlow.map { it.hasInternet() }.stateIn(this)
data class NetworkChangeKey(
val ethernetConnected: Boolean,
val wifiConnected: Boolean,
val cellularConnected: Boolean,
val wifiSsid: String?,
)
connectivityStateFlow
.map {
NetworkChangeKey(
ethernetConnected = it.ethernetConnected,
wifiConnected = it.wifiState.connected,
cellularConnected = it.cellularConnected,
wifiSsid = if (it.wifiState.connected) it.wifiState.ssid else null,
)
}
.distinctUntilChanged()
.stateIn(this)
combine( combine(
settingsRepository.flow.distinctUntilChangedBy { it.appMode }, settingsRepository.flow.distinctUntilChangedBy { it.appMode },
@@ -1,8 +0,0 @@
package com.zaneschepke.wireguardautotunnel.domain.enums
enum class NetworkType {
WIFI,
ETHERNET,
MOBILE_DATA,
NONE,
}
@@ -25,29 +25,27 @@ data class AutoTunnelState(
is NetworkChange, is NetworkChange,
is SettingsChange -> { is SettingsChange -> {
// Compute desired tunnel based on network conditions // Compute desired tunnel based on network conditions
var desiredTunnel: TunnelConfig? = null var preferredTunnel: TunnelConfig? = null
if (networkState.isEthernetConnected && settings.isTunnelOnEthernetEnabled) { if (ethernetActive && settings.isTunnelOnEthernetEnabled) {
desiredTunnel = preferredEthernetTunnel() preferredTunnel = preferredEthernetTunnel()
} else if (isMobileDataActive() && settings.isTunnelOnMobileDataEnabled) { } else if (mobileDataActive && settings.isTunnelOnMobileDataEnabled) {
desiredTunnel = preferredMobileDataTunnel() preferredTunnel = preferredMobileDataTunnel()
} else if ( } else if (wifiActive && settings.isTunnelOnWifiEnabled && !isWifiTrusted()) {
isWifiActive() && settings.isTunnelOnWifiEnabled && !isCurrentSSIDTrusted() preferredTunnel = preferredWifiTunnel()
) {
desiredTunnel = preferredWifiTunnel()
} }
// Override for no connectivity if enabled // Override for no connectivity if enabled
if (isNoConnectivity() && settings.isStopOnNoInternetEnabled) { if (!networkState.hasInternet() && settings.isStopOnNoInternetEnabled) {
desiredTunnel = null preferredTunnel = null
} }
// Determine current active tunnel (assuming only one can be active) // Determine current active tunnel (assuming only one can be active)
val currentTunnel = activeTunnels.entries.firstOrNull()?.key val currentTunnel = activeTunnels.entries.firstOrNull()?.key
// Handle tunnel start/stop/change // Handle tunnel start/stop/change
if (desiredTunnel != null) { if (preferredTunnel != null) {
if (currentTunnel != desiredTunnel.id) { if (currentTunnel != preferredTunnel.id) {
return Start(desiredTunnel) return Start(preferredTunnel)
} }
} else { } else {
if (currentTunnel != null) { if (currentTunnel != null) {
@@ -61,12 +59,9 @@ data class AutoTunnelState(
return DoNothing return DoNothing
} }
// also need to check for Wi-Fi state as there is some overlap when they are both connected private val ethernetActive: Boolean = networkState.activeNetwork is ActiveNetwork.Cellular
private fun isMobileDataActive(): Boolean { private val mobileDataActive: Boolean = networkState.activeNetwork is ActiveNetwork.Ethernet
return !networkState.isEthernetConnected && private val wifiActive: Boolean = networkState.activeNetwork is ActiveNetwork.Wifi
!networkState.isWifiConnected &&
networkState.isMobileDataConnected
}
private fun preferredMobileDataTunnel(): TunnelConfig? { private fun preferredMobileDataTunnel(): TunnelConfig? {
return tunnels.firstOrNull { it.isMobileDataTunnel } return tunnels.firstOrNull { it.isMobileDataTunnel }
@@ -81,27 +76,21 @@ data class AutoTunnelState(
} }
private fun preferredWifiTunnel(): TunnelConfig? { private fun preferredWifiTunnel(): TunnelConfig? {
return getTunnelWithMatchingTunnelNetwork() return getTunnelWithMappedNetwork()
?: tunnels.firstOrNull { it.isPrimaryTunnel } ?: tunnels.firstOrNull { it.isPrimaryTunnel }
?: tunnels.firstOrNull() ?: tunnels.firstOrNull()
} }
// ignore cellular state as there is overlap where it may still be active, but not prioritized private fun isWifiTrusted(): Boolean {
private fun isWifiActive(): Boolean { return with(networkState.activeNetwork) {
return !networkState.isEthernetConnected && networkState.isWifiConnected this is ActiveNetwork.Wifi && isTrustedNetwork(this.ssid)
}
} }
private fun isNoConnectivity(): Boolean { private fun isTrustedNetwork(ssid: String): Boolean =
return !networkState.isEthernetConnected && hasMatch(ssid, settings.trustedNetworkSSIDs)
!networkState.isWifiConnected &&
!networkState.isMobileDataConnected
}
private fun isCurrentSSIDTrusted(): Boolean { private fun hasMatch(
return networkState.wifiName?.let { hasTrustedWifiName(it) } == true
}
private fun hasTrustedWifiName(
wifiName: String, wifiName: String,
wifiNames: Set<String> = settings.trustedNetworkSSIDs, wifiNames: Set<String> = settings.trustedNetworkSSIDs,
): Boolean { ): Boolean {
@@ -112,9 +101,10 @@ data class AutoTunnelState(
} }
} }
private fun getTunnelWithMatchingTunnelNetwork(): TunnelConfig? { private fun getTunnelWithMappedNetwork(): TunnelConfig? =
return networkState.wifiName?.let { wifiName -> when (val network = networkState.activeNetwork) {
tunnels.firstOrNull { hasTrustedWifiName(wifiName, it.tunnelNetworks) } is ActiveNetwork.Wifi ->
tunnels.firstOrNull { hasMatch(network.ssid, it.tunnelNetworks) }
else -> null
} }
}
} }
@@ -1,9 +0,0 @@
package com.zaneschepke.wireguardautotunnel.domain.state
data class ConnectivityState(
val wifiAvailable: Boolean,
val ethernetAvailable: Boolean,
val cellularAvailable: Boolean,
) {
val allOffline = !wifiAvailable && !ethernetAvailable && !cellularAvailable
}
@@ -1,38 +1,48 @@
package com.zaneschepke.wireguardautotunnel.domain.state package com.zaneschepke.wireguardautotunnel.domain.state
import com.zaneschepke.networkmonitor.ActiveNetwork as MonitorActiveNetwork
import com.zaneschepke.networkmonitor.ConnectivityState import com.zaneschepke.networkmonitor.ConnectivityState
import com.zaneschepke.networkmonitor.util.WifiSecurityType import com.zaneschepke.networkmonitor.util.WifiSecurityType
data class NetworkState( sealed class ActiveNetwork {
val isWifiConnected: Boolean = false, data object Disconnected : ActiveNetwork()
val isMobileDataConnected: Boolean = false,
val isEthernetConnected: Boolean = false,
val wifiName: String? = null,
val isWifiSecure: Boolean? = null,
val locationServicesEnabled: Boolean? = null,
val locationPermissionGranted: Boolean? = null,
) {
fun hasNoCapabilities(): Boolean {
return !isWifiConnected && !isMobileDataConnected && !isEthernetConnected
}
companion object { data object Ethernet : ActiveNetwork()
fun from(connectivityState: ConnectivityState): NetworkState {
return NetworkState( data object Cellular : ActiveNetwork()
isWifiSecure =
when (connectivityState.wifiState.securityType) { data class Wifi(val ssid: String, val isSecure: Boolean?) : ActiveNetwork()
}
data class NetworkState(
val activeNetwork: ActiveNetwork = ActiveNetwork.Disconnected,
val locationServicesEnabled: Boolean = false,
val locationPermissionGranted: Boolean = false,
) {
fun hasInternet(): Boolean = activeNetwork !is ActiveNetwork.Disconnected
}
fun ConnectivityState.toDomain(): NetworkState {
val domainNetwork: ActiveNetwork =
when (val network = this.activeNetwork) {
is MonitorActiveNetwork.Wifi -> {
val isSecure =
when (network.securityType) {
WifiSecurityType.OPEN, WifiSecurityType.OPEN,
WifiSecurityType.UNKNOWN -> false WifiSecurityType.UNKNOWN -> false
null -> null null -> null
else -> true else -> true
}, }
isWifiConnected = connectivityState.wifiState.connected, ActiveNetwork.Wifi(ssid = network.ssid, isSecure = isSecure)
isMobileDataConnected = connectivityState.cellularConnected, }
isEthernetConnected = connectivityState.ethernetConnected, is MonitorActiveNetwork.Cellular -> ActiveNetwork.Cellular
wifiName = connectivityState.wifiState.ssid, is MonitorActiveNetwork.Ethernet -> ActiveNetwork.Ethernet
locationPermissionGranted = connectivityState.wifiState.locationPermissionsGranted, is MonitorActiveNetwork.Disconnected -> ActiveNetwork.Disconnected
locationServicesEnabled = connectivityState.wifiState.locationServicesEnabled,
)
} }
}
return NetworkState(
activeNetwork = domainNetwork,
locationPermissionGranted = this.locationPermissionsGranted,
locationServicesEnabled = this.locationServicesEnabled,
)
} }
@@ -36,8 +36,8 @@ import androidx.core.net.toUri
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.zaneschepke.networkmonitor.ActiveNetwork
import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.domain.enums.NetworkType
import com.zaneschepke.wireguardautotunnel.ui.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm import com.zaneschepke.wireguardautotunnel.ui.LocalSharedVm
import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow import com.zaneschepke.wireguardautotunnel.ui.common.button.SurfaceRow
@@ -59,9 +59,9 @@ fun AutoTunnelScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
val clipboard = rememberClipboardHelper() val clipboard = rememberClipboardHelper()
val sharedUiState by shareViewModel.container.stateFlow.collectAsStateWithLifecycle() val sharedUiState by shareViewModel.container.stateFlow.collectAsStateWithLifecycle()
val autoTunnelState by viewModel.container.stateFlow.collectAsStateWithLifecycle() val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
if (autoTunnelState.isLoading) return if (uiState.isLoading) return
val batteryActivity = val batteryActivity =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
@@ -79,11 +79,11 @@ fun AutoTunnelScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
} }
val (ethernetTunnel, mobileDataTunnel, mappedTunnels) = val (ethernetTunnel, mobileDataTunnel, mappedTunnels) =
remember(autoTunnelState.tunnels) { remember(uiState.tunnels) {
Triple( Triple(
autoTunnelState.tunnels.firstOrNull { it.isEthernetTunnel }, uiState.tunnels.firstOrNull { it.isEthernetTunnel },
autoTunnelState.tunnels.firstOrNull { it.isMobileDataTunnel }, uiState.tunnels.firstOrNull { it.isMobileDataTunnel },
autoTunnelState.tunnels.any { it.tunnelNetworks.isNotEmpty() }, uiState.tunnels.any { it.tunnelNetworks.isNotEmpty() },
) )
} }
@@ -94,8 +94,8 @@ fun AutoTunnelScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
) { ) {
Column { Column {
val (title, buttonText, icon) = val (title, buttonText, icon) =
remember(autoTunnelState.autoTunnelActive) { remember(uiState.autoTunnelActive) {
when (autoTunnelState.autoTunnelActive) { when (uiState.autoTunnelActive) {
true -> true ->
Triple( Triple(
context.getString(R.string.auto_tunnel_running), context.getString(R.string.auto_tunnel_running),
@@ -140,35 +140,20 @@ fun AutoTunnelScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
stringResource(R.string.networks), stringResource(R.string.networks),
modifier = Modifier.padding(horizontal = 16.dp), modifier = Modifier.padding(horizontal = 16.dp),
) )
val activeNetworkType by
remember(autoTunnelState.connectivityState) { val localizedNetworkType by
remember(uiState.connectivityState) {
derivedStateOf { derivedStateOf {
val connectivity = autoTunnelState.connectivityState when (uiState.connectivityState?.activeNetwork) {
when { is ActiveNetwork.Wifi -> context.getString(R.string.wifi)
connectivity?.ethernetConnected == true -> NetworkType.ETHERNET is ActiveNetwork.Ethernet -> context.getString(R.string.ethernet)
connectivity?.wifiState?.connected == true -> NetworkType.WIFI is ActiveNetwork.Cellular -> context.getString(R.string.mobile_data)
connectivity?.cellularConnected == true -> NetworkType.MOBILE_DATA is ActiveNetwork.Disconnected -> context.getString(R.string.no_network)
else -> NetworkType.NONE null -> context.getString(R.string.no_network)
} }
} }
} }
val localizedNetworkType =
when (activeNetworkType) {
NetworkType.WIFI -> stringResource(R.string.wifi)
NetworkType.ETHERNET -> stringResource(R.string.ethernet)
NetworkType.MOBILE_DATA -> stringResource(R.string.mobile_data)
NetworkType.NONE -> stringResource(R.string.no_network)
}
val ssid by
remember(autoTunnelState.connectivityState) {
derivedStateOf {
autoTunnelState.connectivityState?.wifiState?.ssid
?: context.getString(R.string.unknown)
}
}
SurfaceRow( SurfaceRow(
leading = { leading = {
Icon(ImageVector.vectorResource(R.drawable.globe), contentDescription = null) Icon(ImageVector.vectorResource(R.drawable.globe), contentDescription = null)
@@ -181,7 +166,7 @@ fun AutoTunnelScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
} }
}, },
description = description =
if (activeNetworkType == NetworkType.WIFI) { (uiState.connectivityState?.activeNetwork as? ActiveNetwork.Wifi)?.let {
{ {
Column { Column {
DescriptionText( DescriptionText(
@@ -189,10 +174,8 @@ fun AutoTunnelScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
append(stringResource(R.string.security_type)) append(stringResource(R.string.security_type))
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
append( append(
autoTunnelState.connectivityState it.securityType?.name
?.wifiState ?: stringResource(R.string.unknown)
?.securityType
?.name ?: stringResource(R.string.unknown)
) )
} }
} }
@@ -201,21 +184,24 @@ fun AutoTunnelScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
buildAnnotatedString { buildAnnotatedString {
append(stringResource(R.string.network_name)) append(stringResource(R.string.network_name))
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
append(ssid) append(it.ssid)
} }
} }
) )
} }
} }
} else null, },
trailing = trailing =
if (activeNetworkType == NetworkType.WIFI) { if (uiState.connectivityState?.activeNetwork is ActiveNetwork.Wifi) {
{ Icon(Icons.Outlined.ContentCopy, contentDescription = null) } { Icon(Icons.Outlined.ContentCopy, contentDescription = null) }
} else null, } else null,
onClick = onClick = {
if (activeNetworkType == NetworkType.WIFI) { when (val network = uiState.connectivityState?.activeNetwork) {
{ clipboard.copy(ssid, context.getString(R.string.wifi)) } is ActiveNetwork.Wifi ->
} else null, clipboard.copy(network.ssid, context.getString(R.string.wifi))
else -> Unit
}
},
) )
SurfaceRow( SurfaceRow(
@@ -223,7 +209,7 @@ fun AutoTunnelScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
title = stringResource(R.string.tunnel_on_wifi), title = stringResource(R.string.tunnel_on_wifi),
trailing = { modifier -> trailing = { modifier ->
SwitchWithDivider( SwitchWithDivider(
checked = autoTunnelState.autoTunnelSettings.isTunnelOnWifiEnabled, checked = uiState.autoTunnelSettings.isTunnelOnWifiEnabled,
onClick = { viewModel.setAutoTunnelOnWifiEnabled(it) }, onClick = { viewModel.setAutoTunnelOnWifiEnabled(it) },
modifier = modifier, modifier = modifier,
) )
@@ -248,7 +234,7 @@ fun AutoTunnelScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
title = stringResource(R.string.tunnel_mobile_data), title = stringResource(R.string.tunnel_mobile_data),
trailing = { modifier -> trailing = { modifier ->
SwitchWithDivider( SwitchWithDivider(
checked = autoTunnelState.autoTunnelSettings.isTunnelOnMobileDataEnabled, checked = uiState.autoTunnelSettings.isTunnelOnMobileDataEnabled,
onClick = { viewModel.setTunnelOnCellular(it) }, onClick = { viewModel.setTunnelOnCellular(it) },
modifier = modifier, modifier = modifier,
) )
@@ -271,7 +257,7 @@ fun AutoTunnelScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
title = stringResource(R.string.tunnel_on_ethernet), title = stringResource(R.string.tunnel_on_ethernet),
trailing = { modifier -> trailing = { modifier ->
SwitchWithDivider( SwitchWithDivider(
checked = autoTunnelState.autoTunnelSettings.isTunnelOnEthernetEnabled, checked = uiState.autoTunnelSettings.isTunnelOnEthernetEnabled,
onClick = { viewModel.setTunnelOnEthernet(it) }, onClick = { viewModel.setTunnelOnEthernet(it) },
modifier = modifier, modifier = modifier,
) )
@@ -295,13 +281,13 @@ fun AutoTunnelScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
description = { DescriptionText(stringResource(R.string.stop_on_internet_loss)) }, description = { DescriptionText(stringResource(R.string.stop_on_internet_loss)) },
trailing = { trailing = {
ThemedSwitch( ThemedSwitch(
checked = autoTunnelState.autoTunnelSettings.isStopOnNoInternetEnabled, checked = uiState.autoTunnelSettings.isStopOnNoInternetEnabled,
onClick = { viewModel.setStopOnNoInternetEnabled(it) }, onClick = { viewModel.setStopOnNoInternetEnabled(it) },
) )
}, },
onClick = { onClick = {
viewModel.setStopOnNoInternetEnabled( viewModel.setStopOnNoInternetEnabled(
!autoTunnelState.autoTunnelSettings.isStopOnNoInternetEnabled !uiState.autoTunnelSettings.isStopOnNoInternetEnabled
) )
}, },
) )
@@ -316,13 +302,11 @@ fun AutoTunnelScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
title = stringResource(R.string.restart_at_boot), title = stringResource(R.string.restart_at_boot),
trailing = { trailing = {
ThemedSwitch( ThemedSwitch(
checked = autoTunnelState.autoTunnelSettings.startOnBoot, checked = uiState.autoTunnelSettings.startOnBoot,
onClick = { viewModel.setStartAtBoot(it) }, onClick = { viewModel.setStartAtBoot(it) },
) )
}, },
onClick = { onClick = { viewModel.setStartAtBoot(!uiState.autoTunnelSettings.startOnBoot) },
viewModel.setStartAtBoot(!autoTunnelState.autoTunnelSettings.startOnBoot)
},
) )
SurfaceRow( SurfaceRow(
leading = { Icon(Icons.Outlined.Settings, contentDescription = null) }, leading = { Icon(Icons.Outlined.Settings, contentDescription = null) },
@@ -47,35 +47,37 @@ fun WifiSettingsScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
val context = LocalContext.current val context = LocalContext.current
val navController = LocalNavController.current val navController = LocalNavController.current
val autoTunnelState by viewModel.container.stateFlow.collectAsStateWithLifecycle() val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle()
if (autoTunnelState.isLoading) return if (uiState.isLoading) return
var showLocationDialog by remember { mutableStateOf(false) } var showLocationDialog by remember { mutableStateOf(false) }
var currentText by rememberSaveable { mutableStateOf("") } var currentText by rememberSaveable { mutableStateOf("") }
LaunchedEffect(autoTunnelState.autoTunnelSettings.trustedNetworkSSIDs) { currentText = "" } LaunchedEffect(uiState.autoTunnelSettings.trustedNetworkSSIDs) { currentText = "" }
val warnings by val warnings by
remember( remember(
autoTunnelState.connectivityState?.wifiState, uiState.connectivityState,
autoTunnelState.autoTunnelSettings.trustedNetworkSSIDs, uiState.autoTunnelSettings.trustedNetworkSSIDs,
autoTunnelState.autoTunnelSettings.wifiDetectionMethod, uiState.autoTunnelSettings.wifiDetectionMethod,
autoTunnelState.tunnels, uiState.tunnels,
) { ) {
derivedStateOf { derivedStateOf {
val wifiState = autoTunnelState.connectivityState?.wifiState
val needsLocation = val needsLocation =
autoTunnelState.autoTunnelSettings.wifiDetectionMethod uiState.autoTunnelSettings.wifiDetectionMethod.needsLocationPermissions()
.needsLocationPermissions()
val hasConfigs = val hasConfigs =
autoTunnelState.autoTunnelSettings.trustedNetworkSSIDs.isNotEmpty() || uiState.autoTunnelSettings.trustedNetworkSSIDs.isNotEmpty() ||
autoTunnelState.tunnels.any { it.tunnelNetworks.isNotEmpty() } uiState.tunnels.any { it.tunnelNetworks.isNotEmpty() }
val showServicesWarning = val showServicesWarning =
(wifiState?.locationServicesEnabled == false) && needsLocation && hasConfigs (uiState.connectivityState?.locationServicesEnabled == false) &&
needsLocation &&
hasConfigs
val showPermissionsWarning = val showPermissionsWarning =
(wifiState?.locationPermissionsGranted == false) && needsLocation && hasConfigs (uiState.connectivityState?.locationPermissionsGranted == false) &&
needsLocation &&
hasConfigs
showServicesWarning to showPermissionsWarning showServicesWarning to showPermissionsWarning
} }
@@ -138,9 +140,7 @@ fun WifiSettingsScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
DescriptionText( DescriptionText(
stringResource( stringResource(
R.string.current_template, R.string.current_template,
autoTunnelState.autoTunnelSettings.wifiDetectionMethod.asTitleString( uiState.autoTunnelSettings.wifiDetectionMethod.asTitleString(context),
context
),
) )
) )
}, },
@@ -157,14 +157,12 @@ fun WifiSettingsScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
}, },
trailing = { trailing = {
ThemedSwitch( ThemedSwitch(
checked = autoTunnelState.autoTunnelSettings.isWildcardsEnabled, checked = uiState.autoTunnelSettings.isWildcardsEnabled,
onClick = { viewModel.setWildcardsEnabled(it) }, onClick = { viewModel.setWildcardsEnabled(it) },
) )
}, },
onClick = { onClick = {
viewModel.setWildcardsEnabled( viewModel.setWildcardsEnabled(!uiState.autoTunnelSettings.isWildcardsEnabled)
!autoTunnelState.autoTunnelSettings.isWildcardsEnabled
)
}, },
) )
} }
@@ -174,14 +172,13 @@ fun WifiSettingsScreen(viewModel: AutoTunnelViewModel = hiltViewModel()) {
title = stringResource(R.string.trusted_wifi_names), title = stringResource(R.string.trusted_wifi_names),
expandedContent = { expandedContent = {
TrustedNetworkTextBox( TrustedNetworkTextBox(
autoTunnelState.autoTunnelSettings.trustedNetworkSSIDs, uiState.autoTunnelSettings.trustedNetworkSSIDs,
onDelete = { viewModel.removeTrustedNetworkName(it) }, onDelete = { viewModel.removeTrustedNetworkName(it) },
currentText = currentText, currentText = currentText,
onSave = { ssid -> viewModel.saveTrustedNetworkName(ssid) }, onSave = { ssid -> viewModel.saveTrustedNetworkName(ssid) },
onValueChange = { currentText = it }, onValueChange = { currentText = it },
supporting = { supporting = {
if (autoTunnelState.autoTunnelSettings.isWildcardsEnabled) if (uiState.autoTunnelSettings.isWildcardsEnabled) WildcardsLabel()
WildcardsLabel()
}, },
modifier = Modifier.padding(top = 4.dp), modifier = Modifier.padding(top = 4.dp),
) )
@@ -41,7 +41,6 @@ class AndroidNetworkMonitor(
companion object { companion object {
const val LOCATION_SERVICES_FILTER: String = "android.location.PROVIDERS_CHANGED" const val LOCATION_SERVICES_FILTER: String = "android.location.PROVIDERS_CHANGED"
const val ANDROID_UNKNOWN_SSID: String = "<unknown ssid>" const val ANDROID_UNKNOWN_SSID: String = "<unknown ssid>"
const val SHELL_COMMAND_TIMEOUT_MS = 2_000L const val SHELL_COMMAND_TIMEOUT_MS = 2_000L
} }
@@ -70,28 +69,31 @@ class AndroidNetworkMonitor(
private val activeWifiNetworks = private val activeWifiNetworks =
ConcurrentHashMap<String, Pair<Network?, NetworkCapabilities?>>() ConcurrentHashMap<String, Pair<Network?, NetworkCapabilities?>>()
private val activeCellularNetworks =
ConcurrentHashMap<String, Pair<Network?, NetworkCapabilities?>>()
private val permissionsChangedFlow = MutableStateFlow(false) private val permissionsChangedFlow = MutableStateFlow(false)
private var permissionReceiver: BroadcastReceiver? = null private var permissionReceiver: BroadcastReceiver? = null
private var locationServicesReceiver: BroadcastReceiver? = null private var locationServicesReceiver: BroadcastReceiver? = null
private var wifiCallback: ConnectivityManager.NetworkCallback? = null private var defaultNetworkCallback: ConnectivityManager.NetworkCallback? = null
private var cellularCallback: ConnectivityManager.NetworkCallback? = null private var wifiInterfaceCallback: ConnectivityManager.NetworkCallback? = null
private var ethernetCallback: ConnectivityManager.NetworkCallback? = null private var cellularInterfaceCallback: ConnectivityManager.NetworkCallback? = null
private var wifiInterfaceCallback: ConnectivityManager.NetworkCallback? = null // NEW
private val isAirplaneModeOn: Boolean
get() =
android.provider.Settings.Global.getInt(
appContext.contentResolver,
android.provider.Settings.Global.AIRPLANE_MODE_ON,
0,
) != 0
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
private val wifiFlow: Flow<TransportEvent> = private val defaultNetworkFlow: Flow<TransportEvent> =
combine(configurationListener.detectionMethod, permissionsChangedFlow) { combine(configurationListener.detectionMethod, permissionsChangedFlow) {
detectionMethod, detectionMethod,
changed -> changed ->
Pair(detectionMethod, changed) Pair(detectionMethod, changed)
} }
.flatMapLatest { (detectionMethod, _) -> // cancels previous flow .flatMapLatest { (detectionMethod, _) ->
Timber.d("Permission or detection method changed, recreating wifiFlow") createDefaultNetworkCallbackFlow(detectionMethod)
createWifiNetworkCallbackFlow(detectionMethod)
} }
private fun isAndroidTv(): Boolean = private fun isAndroidTv(): Boolean =
@@ -120,82 +122,75 @@ class AndroidNetworkMonitor(
return fineLocationGranted && backgroundLocationGranted return fineLocationGranted && backgroundLocationGranted
} }
private fun createWifiNetworkCallbackFlow( private fun createDefaultNetworkCallbackFlow(
detectionMethod: WifiDetectionMethod detectionMethod: WifiDetectionMethod
): Flow<TransportEvent> = callbackFlow { ): Flow<TransportEvent> = callbackFlow {
fun handleOnWifiLost(network: Network) { val callback =
Timber.d("Wi-Fi onLost: network=$network") object : ConnectivityManager.NetworkCallback() {
activeWifiNetworks.remove(network.toString())
if (activeWifiNetworks.isEmpty()) { override fun onAvailable(network: Network) {
Timber.d("All Wi-Fi networks disconnected, clearing currentSsid and wifiConnected") Timber.d("Network onAvailable: network=$network")
trySend(TransportEvent.Lost(network)) trySend(TransportEvent.Unknown)
} else { }
Timber.d("Wi-Fi onLost, but still connected to other networks, ignoring")
// This can happen when switching between APs of the same SSID override fun onLost(network: Network) {
Timber.d("Network onLost: network=$network")
trySend(TransportEvent.Lost(network))
}
override fun onCapabilitiesChanged(
network: Network,
networkCapabilities: 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,
)
}
networkCapabilities.hasTransport(
NetworkCapabilities.TRANSPORT_CELLULAR
) -> {
activeWifiNetworks.clear()
TransportEvent.CapabilitiesChanged(network, networkCapabilities)
}
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))
}
}
} }
} defaultNetworkCallback = callback
fun handleOnWifiAvailable(network: Network) { connectivityManager?.registerDefaultNetworkCallback(defaultNetworkCallback!!)
Timber.d("Wi-Fi onAvailable: network=$network")
activeWifiNetworks[network.toString()] = Pair(network, null)
trySend(TransportEvent.Available(network, detectionMethod))
}
fun handleOnWifiCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities,
) {
Timber.d("Wi-Fi onCapabilitiesChanged: network=$network")
activeWifiNetworks[network.toString()] = Pair(network, networkCapabilities)
trySend(
TransportEvent.CapabilitiesChanged(network, networkCapabilities, detectionMethod)
)
}
wifiCallback =
when {
detectionMethod == WifiDetectionMethod.LEGACY ||
Build.VERSION.SDK_INT < Build.VERSION_CODES.S ->
object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
handleOnWifiAvailable(network)
}
override fun onLost(network: Network) {
handleOnWifiLost(network)
}
}
.also { Timber.d("Creating Wi-Fi callback without location info flags") }
else ->
object : ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) {
override fun onAvailable(network: Network) {
if (detectionMethod != WifiDetectionMethod.DEFAULT)
handleOnWifiAvailable(network)
}
override fun onCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities,
) {
if (detectionMethod == WifiDetectionMethod.DEFAULT)
handleOnWifiCapabilitiesChanged(network, networkCapabilities)
}
override fun onLost(network: Network) {
handleOnWifiLost(network)
}
}
.also { Timber.d("Creating Wi-Fi callback with location info flags") }
}
val request =
NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.build()
connectivityManager?.registerNetworkCallback(request, wifiCallback!!)
trySend( trySend(
TransportEvent.Permissions( TransportEvent.Permissions(
@@ -208,8 +203,8 @@ class AndroidNetworkMonitor(
) )
awaitClose { awaitClose {
runCatching { connectivityManager?.unregisterNetworkCallback(wifiCallback!!) } runCatching { connectivityManager?.unregisterNetworkCallback(defaultNetworkCallback!!) }
.onFailure { Timber.e(it, "Error unregistering network callback") } .onFailure { Timber.e(it, "Error unregistering default network callback") }
} }
} }
@@ -217,18 +212,17 @@ class AndroidNetworkMonitor(
val localCallback = val localCallback =
object : ConnectivityManager.NetworkCallback() { object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) { override fun onAvailable(network: Network) {
Timber.d("Wi-Fi Interface onAvailable (Adapter ON): network=$network") Timber.d("Wi-Fi onAvailable: network=$network")
trySend(true) trySend(true)
} }
override fun onLost(network: Network) { override fun onLost(network: Network) {
Timber.d("Wi-Fi Interface onLost (Adapter OFF): network=$network") Timber.d("Wi-Fi onLost: network=$network")
trySend(false) trySend(false)
} }
} }
wifiInterfaceCallback = localCallback wifiInterfaceCallback = localCallback
// wifi Transport only
val request = val request =
NetworkRequest.Builder().addTransportType(NetworkCapabilities.TRANSPORT_WIFI).build() NetworkRequest.Builder().addTransportType(NetworkCapabilities.TRANSPORT_WIFI).build()
@@ -243,78 +237,64 @@ class AndroidNetworkMonitor(
} }
} }
private val cellularFlow: Flow<TransportEvent> = callbackFlow { private val cellularInterfaceFlow: Flow<Boolean> = callbackFlow {
val cellularLocalCallback = val localCallback =
object : ConnectivityManager.NetworkCallback() { object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) { override fun onAvailable(network: Network) {
Timber.d("Cellular onAvailable: network=$network") Timber.d("Cellular onAvailable: network=$network")
activeCellularNetworks[network.toString()] = Pair(network, null) trySend(true)
trySend(TransportEvent.Available(network))
} }
override fun onLost(network: Network) { override fun onLost(network: Network) {
Timber.d("Cellular onLost: network=$network") Timber.d("Cellular onLost: network=$network")
activeCellularNetworks.remove(network.toString()) trySend(false)
if (activeCellularNetworks.isEmpty()) {
Timber.d("All cellular networks disconnected")
trySend(TransportEvent.Lost(network))
} else {
Timber.d("Cellular onLost, but still connected to other, ignoring")
}
}
override fun onCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities,
) {
Timber.d("Cellular onCapabilitiesChanged: network=$network")
activeCellularNetworks[network.toString()] = Pair(network, networkCapabilities)
} }
} }
cellularCallback = cellularLocalCallback cellularInterfaceCallback = localCallback
val request = val request =
NetworkRequest.Builder() NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
.build() .build()
connectivityManager?.registerNetworkCallback(request, cellularCallback!!) connectivityManager?.registerNetworkCallback(request, cellularInterfaceCallback!!)
trySend(TransportEvent.Unknown)
// initial state
val initialCellularNetwork = connectivityManager?.activeNetwork
val initialCapabilities =
connectivityManager?.getNetworkCapabilities(initialCellularNetwork)
val isCellularInitiallyOn =
initialCapabilities?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true
trySend(isCellularInitiallyOn)
awaitClose { awaitClose {
runCatching { connectivityManager?.unregisterNetworkCallback(cellularCallback!!) } runCatching {
.onFailure { Timber.e(it, "Error unregistering cellular network callback") } connectivityManager?.unregisterNetworkCallback(cellularInterfaceCallback!!)
}
.onFailure { Timber.e(it, "Error unregistering cellular interface callback") }
} }
} }
private val ethernetFlow: Flow<TransportEvent> = callbackFlow { private val airplaneModeFlow: Flow<Boolean> = callbackFlow {
val ethernetLocalCallback = val receiver =
object : ConnectivityManager.NetworkCallback() { object : BroadcastReceiver() {
override fun onAvailable(network: Network) { override fun onReceive(context: Context, intent: Intent) {
Timber.d("Ethernet onAvailable: network=$network") if (intent.action == Intent.ACTION_AIRPLANE_MODE_CHANGED) {
trySend(TransportEvent.Available(network)) Timber.d("Received airplane mode changed broadcast")
} trySend(isAirplaneModeOn)
}
override fun onLost(network: Network) {
Timber.d("Ethernet onLost: network=$network")
trySend(TransportEvent.Lost(network))
} }
} }
ethernetCallback = ethernetLocalCallback
val request = val filter = IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED)
NetworkRequest.Builder() appContext.registerReceiver(receiver, filter)
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
.build()
connectivityManager?.registerNetworkCallback(request, ethernetCallback!!) // initial state
trySend(TransportEvent.Unknown) trySend(isAirplaneModeOn)
awaitClose { awaitClose {
runCatching { connectivityManager?.unregisterNetworkCallback(ethernetCallback!!) } runCatching { appContext.unregisterReceiver(receiver) }
.onFailure { Timber.e(it, "Error unregistering ethernet network callback") } .onFailure { Timber.e(it, "Error unregistering airplane mode receiver") }
} }
} }
@@ -350,43 +330,54 @@ class AndroidNetworkMonitor(
.also { Timber.d("Current SSID via ${method.name}: $it") } .also { Timber.d("Current SSID via ${method.name}: $it") }
} }
// prevent false positive late mobile data changes to combat android api quirks
private fun isLateCellularChange(previous: ConnectivityState, new: ConnectivityState): Boolean {
return (previous.wifiState.connected != new.wifiState.connected &&
previous.wifiState.ssid == new.wifiState.ssid &&
previous.cellularConnected != new.cellularConnected)
}
override val connectivityStateFlow: SharedFlow<ConnectivityState> = override val connectivityStateFlow: SharedFlow<ConnectivityState> =
combine( combine(
wifiFlow.scan( defaultNetworkFlow.scan(
WifiState( ConnectivityState(
activeNetwork = ActiveNetwork.Disconnected,
locationPermissionsGranted = hasRequiredLocationPermissions(), locationPermissionsGranted = hasRequiredLocationPermissions(),
locationServicesEnabled = locationServicesEnabled =
locationManager?.isLocationServicesEnabled() ?: false, locationManager?.isLocationServicesEnabled() ?: false,
) )
) { previous, event -> ) { previous, event ->
when (event) { when (event) {
is TransportEvent.Available -> is TransportEvent.CapabilitiesChanged -> {
previous.copy( when {
connected = true, event.networkCapabilities.hasTransport(
ssid = NetworkCapabilities.TRANSPORT_WIFI
getSsidByDetectionMethod( ) -> {
event.wifiDetectionMethod ?: WifiDetectionMethod.DEFAULT, val ssid =
null, getSsidByDetectionMethod(
), event.wifiDetectionMethod
securityType = wifiManager?.getCurrentSecurityType(), ?: WifiDetectionMethod.DEFAULT,
) event.networkCapabilities,
is TransportEvent.CapabilitiesChanged -> )
previous.copy(
connected = true, previous.copy(
ssid = activeNetwork =
getSsidByDetectionMethod( ActiveNetwork.Wifi(
event.wifiDetectionMethod ?: WifiDetectionMethod.DEFAULT, ssid = ssid,
null, securityType = wifiManager?.getCurrentSecurityType(),
), )
securityType = wifiManager?.getCurrentSecurityType(), )
) }
event.networkCapabilities.hasTransport(
NetworkCapabilities.TRANSPORT_CELLULAR
) -> {
activeWifiNetworks.clear()
previous.copy(activeNetwork = ActiveNetwork.Cellular)
}
event.networkCapabilities.hasTransport(
NetworkCapabilities.TRANSPORT_ETHERNET
) -> {
activeWifiNetworks.clear()
previous.copy(activeNetwork = ActiveNetwork.Ethernet)
}
else -> previous
}
}
is TransportEvent.Lost ->
previous.copy(activeNetwork = ActiveNetwork.Disconnected)
is TransportEvent.Permissions -> { is TransportEvent.Permissions -> {
previous.copy( previous.copy(
locationPermissionsGranted = locationPermissionsGranted =
@@ -394,49 +385,36 @@ class AndroidNetworkMonitor(
locationServicesEnabled = event.permissions.locationServicesEnabled, locationServicesEnabled = event.permissions.locationServicesEnabled,
) )
} }
is TransportEvent.Lost -> is TransportEvent.Available -> previous
previous.copy(connected = false, securityType = null, ssid = null)
is TransportEvent.Unknown -> previous is TransportEvent.Unknown -> previous
} }
}, },
cellularFlow,
ethernetFlow,
wifiInterfaceFlow, wifiInterfaceFlow,
) { wifiState, cellular, ethernet, isWifiInterfaceOn -> airplaneModeFlow,
val cellularConnected = cellular is TransportEvent.Available cellularInterfaceFlow,
val ethernetConnected = ethernet is TransportEvent.Available ) { defaultState, isWifiInterfaceOn, isAirplaneModeOn, isCellularInterfaceOn ->
val activeNetwork =
// if wifi is off, force wifi state to disconnected when {
val finalWifiState = // Wi-Fi interface disabled, force disconnected
if (!isWifiInterfaceOn) { !isWifiInterfaceOn && defaultState.activeNetwork is ActiveNetwork.Wifi ->
wifiState.copy(connected = false, securityType = null, ssid = null) ActiveNetwork.Disconnected
} else { // Cellular active when airplane mode on
wifiState 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( ConnectivityState(
finalWifiState, activeNetwork = activeNetwork,
cellularConnected = cellularConnected, locationPermissionsGranted = defaultState.locationPermissionsGranted,
ethernetConnected = ethernetConnected, locationServicesEnabled = defaultState.locationServicesEnabled,
) )
.also { Timber.i("Connectivity Status: $it") } .also { Timber.i("Connectivity Status: $it") }
} }
.scan(
ConnectivityState(
WifiState(
locationPermissionsGranted = hasRequiredLocationPermissions(),
locationServicesEnabled =
locationManager?.isLocationServicesEnabled() ?: false,
)
)
) { previous, current ->
if (isLateCellularChange(previous, current)) {
Timber.d("Skipping late cellular change")
previous
} else {
current
}
}
.distinctUntilChanged() .distinctUntilChanged()
.shareIn(applicationScope, SharingStarted.Eagerly, replay = 1) .shareIn(applicationScope, SharingStarted.Eagerly, replay = 1)
@@ -450,7 +428,7 @@ class AndroidNetworkMonitor(
init { init {
val receiverFlags = val receiverFlags =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Context.RECEIVER_EXPORTED // System broadcast Context.RECEIVER_EXPORTED
} else { } else {
0 0
} }
@@ -461,11 +439,9 @@ class AndroidNetworkMonitor(
if (intent.action == actionPermissionCheck) { if (intent.action == actionPermissionCheck) {
val isGranted = hasRequiredLocationPermissions() val isGranted = hasRequiredLocationPermissions()
Timber.d("Received permission check broadcast, isGranted: $isGranted") Timber.d("Received permission check broadcast, isGranted: $isGranted")
// get Wi-Fi info on permission change and update permission state
if ( if (
connectivityStateFlow.replayCache connectivityStateFlow.replayCache
.firstOrNull() .firstOrNull()
?.wifiState
?.locationPermissionsGranted != isGranted ?.locationPermissionsGranted != isGranted
) { ) {
Timber.d( Timber.d(
@@ -491,13 +467,11 @@ class AndroidNetworkMonitor(
if ( if (
connectivityStateFlow.replayCache connectivityStateFlow.replayCache
.firstOrNull() .firstOrNull()
?.wifiState
?.locationServicesEnabled != isLocationServicesEnabled ?.locationServicesEnabled != isLocationServicesEnabled
) { ) {
Timber.d( Timber.d(
"Location services have changed, canceling and restarting callback flow" "Location services have changed, canceling and restarting callback flow"
) )
// trigger cancel and recreate of callbackFlow
activeWifiNetworks.clear() activeWifiNetworks.clear()
permissionsChangedFlow.update { !permissionsChangedFlow.value } permissionsChangedFlow.update { !permissionsChangedFlow.value }
} }
@@ -518,12 +492,11 @@ class AndroidNetworkMonitor(
permissionReceiver?.let { appContext.unregisterReceiver(it) } permissionReceiver?.let { appContext.unregisterReceiver(it) }
locationServicesReceiver?.let { appContext.unregisterReceiver(it) } locationServicesReceiver?.let { appContext.unregisterReceiver(it) }
wifiCallback?.let { connectivityManager?.unregisterNetworkCallback(it) } defaultNetworkCallback?.let { connectivityManager?.unregisterNetworkCallback(it) }
cellularCallback?.let { connectivityManager?.unregisterNetworkCallback(it) } wifiInterfaceCallback?.let { connectivityManager?.unregisterNetworkCallback(it) }
ethernetCallback?.let { connectivityManager?.unregisterNetworkCallback(it) } cellularInterfaceCallback?.let {
wifiInterfaceCallback?.let {
connectivityManager?.unregisterNetworkCallback(it) connectivityManager?.unregisterNetworkCallback(it)
} // NEW }
} }
.onFailure { Timber.e(it, "Error during cleanup") } .onFailure { Timber.e(it, "Error during cleanup") }
Timber.d("NetworkMonitor cleaned up") Timber.d("NetworkMonitor cleaned up")
@@ -3,22 +3,38 @@ package com.zaneschepke.networkmonitor
import com.zaneschepke.networkmonitor.util.WifiSecurityType import com.zaneschepke.networkmonitor.util.WifiSecurityType
data class ConnectivityState( data class ConnectivityState(
val wifiState: WifiState, val activeNetwork: ActiveNetwork,
val ethernetConnected: Boolean = false,
val cellularConnected: Boolean = false,
) {
fun hasConnectivity(): Boolean = wifiState.connected || ethernetConnected || cellularConnected
}
data class WifiState(
val connected: Boolean = false,
val ssid: String? = null,
val securityType: WifiSecurityType? = null,
val locationPermissionsGranted: Boolean, val locationPermissionsGranted: Boolean,
val locationServicesEnabled: Boolean, val locationServicesEnabled: Boolean,
) { ) {
override fun toString(): String = fun hasInternet(): Boolean = activeNetwork !is ActiveNetwork.Disconnected
"connected=$connected, ssid=${if(ssid == AndroidNetworkMonitor.ANDROID_UNKNOWN_SSID || ssid == null) ssid else ssid.first() + "..."} securityType=$securityType, locationPermissionsGranted=$locationPermissionsGranted"
override fun toString(): String {
val networkInfo =
when (activeNetwork) {
is ActiveNetwork.Disconnected -> "Disconnected"
is ActiveNetwork.Ethernet -> "Ethernet"
is ActiveNetwork.Cellular -> "Cellular"
is ActiveNetwork.Wifi -> {
val ssidDisplay =
if (activeNetwork.ssid == AndroidNetworkMonitor.ANDROID_UNKNOWN_SSID)
activeNetwork.ssid
else activeNetwork.ssid.first() + "..."
"Wifi(ssid=$ssidDisplay, securityType=${activeNetwork.securityType})"
}
}
return "activeNetwork=$networkInfo, locationPermissionsGranted=$locationPermissionsGranted, locationServicesEnabled=$locationServicesEnabled"
}
}
sealed class ActiveNetwork {
data object Disconnected : ActiveNetwork()
data object Ethernet : ActiveNetwork()
data object Cellular : ActiveNetwork()
data class Wifi(val ssid: String, val securityType: WifiSecurityType? = null) : ActiveNetwork()
} }
data class Permissions( data class Permissions(