mirror of
https://github.com/wgtunnel/android.git
synced 2026-06-02 00:29:08 +02:00
+4
-4
@@ -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,
|
|
||||||
}
|
|
||||||
+28
-38
@@ -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
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
-9
@@ -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
|
|
||||||
}
|
|
||||||
+36
-26
@@ -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,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+38
-54
@@ -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) },
|
||||||
|
|||||||
+21
-24
@@ -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),
|
||||||
)
|
)
|
||||||
|
|||||||
+180
-207
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user