mirror of
https://github.com/wgtunnel/android.git
synced 2026-06-02 08:33:40 +02:00
+102
-81
@@ -15,10 +15,11 @@ import com.wireguard.android.util.RootShell
|
|||||||
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor.WifiDetectionMethod.*
|
import com.zaneschepke.networkmonitor.AndroidNetworkMonitor.WifiDetectionMethod.*
|
||||||
import com.zaneschepke.networkmonitor.shizuku.ShizukuShell
|
import com.zaneschepke.networkmonitor.shizuku.ShizukuShell
|
||||||
import com.zaneschepke.networkmonitor.util.*
|
import com.zaneschepke.networkmonitor.util.*
|
||||||
|
import kotlin.concurrent.atomics.AtomicReference
|
||||||
|
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
@@ -200,7 +201,14 @@ class AndroidNetworkMonitor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val request =
|
val request =
|
||||||
NetworkRequest.Builder().addTransportType(NetworkCapabilities.TRANSPORT_WIFI).build()
|
NetworkRequest.Builder()
|
||||||
|
.apply {
|
||||||
|
addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
||||||
|
// remove so we can detect underlying network info on VPN
|
||||||
|
removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
|
||||||
connectivityManager?.registerNetworkCallback(request, wifiCallback!!)
|
connectivityManager?.registerNetworkCallback(request, wifiCallback!!)
|
||||||
|
|
||||||
awaitClose {
|
awaitClose {
|
||||||
@@ -234,8 +242,13 @@ class AndroidNetworkMonitor(
|
|||||||
|
|
||||||
val request =
|
val request =
|
||||||
NetworkRequest.Builder()
|
NetworkRequest.Builder()
|
||||||
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
|
.apply {
|
||||||
|
addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||||
|
// remove so we can detect underlying network info on VPN
|
||||||
|
removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
|
||||||
|
}
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
connectivityManager?.registerNetworkCallback(request, cellularCallback!!)
|
connectivityManager?.registerNetworkCallback(request, cellularCallback!!)
|
||||||
|
|
||||||
trySend(TransportEvent.Unknown)
|
trySend(TransportEvent.Unknown)
|
||||||
@@ -271,8 +284,13 @@ class AndroidNetworkMonitor(
|
|||||||
|
|
||||||
val request =
|
val request =
|
||||||
NetworkRequest.Builder()
|
NetworkRequest.Builder()
|
||||||
.addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
|
.apply {
|
||||||
|
addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
|
||||||
|
// remove so we can detect underlying network info on VPN
|
||||||
|
removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
|
||||||
|
}
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
connectivityManager?.registerNetworkCallback(request, ethernetCallback!!)
|
connectivityManager?.registerNetworkCallback(request, ethernetCallback!!)
|
||||||
|
|
||||||
trySend(TransportEvent.Unknown)
|
trySend(TransportEvent.Unknown)
|
||||||
@@ -366,7 +384,11 @@ class AndroidNetworkMonitor(
|
|||||||
NetworkData(defaultEvent, wifiCaps, cellularCaps, ethernetCaps)
|
NetworkData(defaultEvent, wifiCaps, cellularCaps, ethernetCaps)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
// tracking to prevent races that occur when VPN is first activated
|
||||||
|
private val lastKnownActiveNetwork = MutableStateFlow<ActiveNetwork>(ActiveNetwork.Disconnected)
|
||||||
|
@OptIn(ExperimentalAtomicApi::class) private val vpnActiveState = AtomicReference(false)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class, ExperimentalAtomicApi::class)
|
||||||
override val connectivityStateFlow: SharedFlow<ConnectivityState> =
|
override val connectivityStateFlow: SharedFlow<ConnectivityState> =
|
||||||
combine(networkFlows, airplaneModeFlow, configurationListener.detectionMethod) {
|
combine(networkFlows, airplaneModeFlow, configurationListener.detectionMethod) {
|
||||||
networkData,
|
networkData,
|
||||||
@@ -377,7 +399,6 @@ class AndroidNetworkMonitor(
|
|||||||
val cellularCaps = networkData.cellularCaps
|
val cellularCaps = networkData.cellularCaps
|
||||||
val ethernetCaps = networkData.ethernetCaps
|
val ethernetCaps = networkData.ethernetCaps
|
||||||
|
|
||||||
// get the latest permissions info
|
|
||||||
val permissions =
|
val permissions =
|
||||||
when (defaultEvent) {
|
when (defaultEvent) {
|
||||||
is TransportEvent.Permissions -> defaultEvent.permissions
|
is TransportEvent.Permissions -> defaultEvent.permissions
|
||||||
@@ -388,98 +409,98 @@ class AndroidNetworkMonitor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val androidActiveNetwork = connectivityManager?.activeNetwork
|
// determine default network capabilities
|
||||||
val defaultCaps =
|
val defaultCaps =
|
||||||
when (defaultEvent) {
|
when (defaultEvent) {
|
||||||
is TransportEvent.CapabilitiesChanged -> defaultEvent.networkCapabilities
|
is TransportEvent.CapabilitiesChanged -> defaultEvent.networkCapabilities
|
||||||
else ->
|
else ->
|
||||||
androidActiveNetwork?.let {
|
connectivityManager?.activeNetwork?.let {
|
||||||
connectivityManager?.getNetworkCapabilities(it)
|
connectivityManager.getNetworkCapabilities(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
?: return@combine ConnectivityState(
|
?: return@combine ConnectivityState(
|
||||||
ActiveNetwork.Disconnected,
|
activeNetwork = ActiveNetwork.Disconnected,
|
||||||
permissions.locationServicesEnabled,
|
locationPermissionsGranted = permissions.locationPermissionGranted,
|
||||||
permissions.locationPermissionGranted,
|
locationServicesEnabled = permissions.locationServicesEnabled,
|
||||||
isVpnActive = false,
|
vpnState = VpnState.Inactive,
|
||||||
)
|
)
|
||||||
|
|
||||||
val vpnActive = defaultCaps.hasTransport(NetworkCapabilities.TRANSPORT_VPN)
|
val vpnPreviouslyActive =
|
||||||
|
vpnActiveState.exchange(
|
||||||
// determine underlying network capabilities in order of Android's priority
|
defaultCaps.hasTransport(NetworkCapabilities.TRANSPORT_VPN)
|
||||||
// (Ethernet > Wi-Fi > Cellular)
|
|
||||||
val underlyingCaps = ethernetCaps ?: wifiCaps ?: cellularCaps
|
|
||||||
|
|
||||||
// default caps will have detailed network info if VPN is not active
|
|
||||||
val capsForValidation =
|
|
||||||
if (vpnActive) underlyingCaps ?: defaultCaps else defaultCaps
|
|
||||||
|
|
||||||
// ensure validated internet connectivity
|
|
||||||
// there is a known issue where Android will still report cellular connectivity if
|
|
||||||
// the
|
|
||||||
// interface is not disabled and there is no connectivity (denoted by the '!')
|
|
||||||
val isValidated =
|
|
||||||
capsForValidation.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
|
||||||
val hasInternet =
|
|
||||||
capsForValidation.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
|
||||||
|
|
||||||
if (!isValidated || !hasInternet || (vpnActive && underlyingCaps == null)) {
|
|
||||||
return@combine ConnectivityState(
|
|
||||||
ActiveNetwork.Disconnected,
|
|
||||||
permissions.locationServicesEnabled,
|
|
||||||
permissions.locationPermissionGranted,
|
|
||||||
isVpnActive = vpnActive,
|
|
||||||
)
|
)
|
||||||
}
|
val isVpnActive = vpnActiveState.load()
|
||||||
|
|
||||||
val activeNetwork: ActiveNetwork =
|
// determine vpn state
|
||||||
// if the VPN is active, we need to rely on capabilities from network flows as
|
val vpnState: VpnState =
|
||||||
// we won't have delayed underlying network info from default
|
if (!isVpnActive) {
|
||||||
if (vpnActive) {
|
VpnState.Inactive
|
||||||
when {
|
|
||||||
ethernetCaps != null -> ActiveNetwork.Ethernet
|
|
||||||
wifiCaps != null -> {
|
|
||||||
val ssid = getSsidByDetectionMethod(detectionMethod, wifiCaps)
|
|
||||||
ActiveNetwork.Wifi(ssid, wifiManager?.getCurrentSecurityType())
|
|
||||||
}
|
|
||||||
isAirplaneOn -> ActiveNetwork.Disconnected
|
|
||||||
cellularCaps != null -> ActiveNetwork.Cellular
|
|
||||||
else -> ActiveNetwork.Disconnected
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// we can rely on default caps when VPN is not active
|
VpnState.Active(
|
||||||
when {
|
hasInternet =
|
||||||
defaultCaps.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) ->
|
defaultCaps.hasCapability(
|
||||||
ActiveNetwork.Ethernet
|
NetworkCapabilities.NET_CAPABILITY_VALIDATED
|
||||||
defaultCaps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> {
|
)
|
||||||
val ssid = getSsidByDetectionMethod(detectionMethod, defaultCaps)
|
)
|
||||||
ActiveNetwork.Wifi(ssid, wifiManager?.getCurrentSecurityType())
|
|
||||||
}
|
|
||||||
defaultCaps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) &&
|
|
||||||
!isAirplaneOn -> ActiveNetwork.Cellular
|
|
||||||
else -> ActiveNetwork.Disconnected
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val activeNetwork: ActiveNetwork =
|
||||||
|
run {
|
||||||
|
if (!isVpnActive) {
|
||||||
|
when {
|
||||||
|
defaultCaps.hasTransport(
|
||||||
|
NetworkCapabilities.TRANSPORT_ETHERNET
|
||||||
|
) -> ActiveNetwork.Ethernet
|
||||||
|
defaultCaps.hasTransport(
|
||||||
|
NetworkCapabilities.TRANSPORT_WIFI
|
||||||
|
) -> {
|
||||||
|
val ssid =
|
||||||
|
getSsidByDetectionMethod(detectionMethod, defaultCaps)
|
||||||
|
ActiveNetwork.Wifi(
|
||||||
|
ssid,
|
||||||
|
wifiManager?.getCurrentSecurityType(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
defaultCaps.hasTransport(
|
||||||
|
NetworkCapabilities.TRANSPORT_CELLULAR
|
||||||
|
) && !isAirplaneOn -> ActiveNetwork.Cellular
|
||||||
|
else -> ActiveNetwork.Disconnected
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val fromCaps =
|
||||||
|
when {
|
||||||
|
ethernetCaps != null -> ActiveNetwork.Ethernet
|
||||||
|
wifiCaps != null -> {
|
||||||
|
val ssid =
|
||||||
|
getSsidByDetectionMethod(detectionMethod, wifiCaps)
|
||||||
|
ActiveNetwork.Wifi(
|
||||||
|
ssid,
|
||||||
|
wifiManager?.getCurrentSecurityType(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
cellularCaps != null && !isAirplaneOn ->
|
||||||
|
ActiveNetwork.Cellular
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
fromCaps
|
||||||
|
?: if (!vpnPreviouslyActive) {
|
||||||
|
lastKnownActiveNetwork.value
|
||||||
|
} else {
|
||||||
|
ActiveNetwork.Disconnected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.also { network -> lastKnownActiveNetwork.value = network }
|
||||||
|
|
||||||
ConnectivityState(
|
ConnectivityState(
|
||||||
activeNetwork,
|
activeNetwork = activeNetwork,
|
||||||
permissions.locationServicesEnabled,
|
locationPermissionsGranted = permissions.locationPermissionGranted,
|
||||||
permissions.locationPermissionGranted,
|
locationServicesEnabled = permissions.locationServicesEnabled,
|
||||||
isVpnActive = vpnActive,
|
vpnState = vpnState,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.flatMapLatest { state ->
|
|
||||||
// prevent disconnected emits when VPN is activated
|
|
||||||
if (state.activeNetwork is ActiveNetwork.Disconnected && state.isVpnActive) {
|
|
||||||
flow {
|
|
||||||
delay(1500)
|
|
||||||
emit(state)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
flowOf(state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.shareIn(applicationScope, SharingStarted.Eagerly, replay = 1)
|
.shareIn(applicationScope, SharingStarted.Eagerly, replay = 1)
|
||||||
|
|
||||||
// utility to send local broadcast to trigger a recheck of location permissions onResume,
|
// utility to send local broadcast to trigger a recheck of location permissions onResume,
|
||||||
@@ -562,7 +583,7 @@ class AndroidNetworkMonitor(
|
|||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
if (intent.action == Intent.ACTION_AIRPLANE_MODE_CHANGED) {
|
if (intent.action == Intent.ACTION_AIRPLANE_MODE_CHANGED) {
|
||||||
Timber.d("Received airplane mode changed broadcast")
|
Timber.d("Received airplane mode changed broadcast")
|
||||||
airplaneModeState.value = appContext.isAirplaneModeOn()
|
airplaneModeState.update { appContext.isAirplaneModeOn() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ data class ConnectivityState(
|
|||||||
val activeNetwork: ActiveNetwork,
|
val activeNetwork: ActiveNetwork,
|
||||||
val locationPermissionsGranted: Boolean,
|
val locationPermissionsGranted: Boolean,
|
||||||
val locationServicesEnabled: Boolean,
|
val locationServicesEnabled: Boolean,
|
||||||
val isVpnActive: Boolean,
|
val vpnState: VpnState,
|
||||||
) {
|
) {
|
||||||
fun hasInternet(): Boolean = activeNetwork !is ActiveNetwork.Disconnected
|
fun hasInternet(): Boolean = activeNetwork !is ActiveNetwork.Disconnected
|
||||||
|
|
||||||
@@ -39,3 +39,9 @@ sealed class ActiveNetwork {
|
|||||||
|
|
||||||
data object Ethernet : ActiveNetwork()
|
data object Ethernet : ActiveNetwork()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sealed interface VpnState {
|
||||||
|
object Inactive : VpnState
|
||||||
|
|
||||||
|
data class Active(val hasInternet: Boolean) : VpnState
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user