@@ -2,26 +2,44 @@ package net.mullvad.talpid
2
2
3
3
import android.net.ConnectivityManager
4
4
import android.net.LinkProperties
5
+ import android.net.Network
6
+ import android.net.NetworkCapabilities
7
+ import co.touchlab.kermit.Logger
8
+ import java.net.DatagramSocket
9
+ import java.net.Inet4Address
10
+ import java.net.Inet6Address
5
11
import java.net.InetAddress
6
12
import kotlin.collections.ArrayList
13
+ import kotlin.time.Duration.Companion.milliseconds
7
14
import kotlinx.coroutines.CoroutineScope
15
+ import kotlinx.coroutines.Dispatchers
16
+ import kotlinx.coroutines.FlowPreview
8
17
import kotlinx.coroutines.channels.Channel
9
18
import kotlinx.coroutines.flow.MutableStateFlow
10
19
import kotlinx.coroutines.flow.SharingStarted
11
20
import kotlinx.coroutines.flow.StateFlow
21
+ import kotlinx.coroutines.flow.debounce
22
+ import kotlinx.coroutines.flow.distinctUntilChanged
23
+ import kotlinx.coroutines.flow.filter
12
24
import kotlinx.coroutines.flow.map
13
25
import kotlinx.coroutines.flow.merge
14
26
import kotlinx.coroutines.flow.onEach
15
27
import kotlinx.coroutines.flow.receiveAsFlow
16
28
import kotlinx.coroutines.flow.stateIn
17
29
import kotlinx.coroutines.launch
30
+ import kotlinx.coroutines.plus
31
+ import kotlinx.coroutines.runBlocking
32
+ import net.mullvad.talpid.model.Connectivity
18
33
import net.mullvad.talpid.model.NetworkState
34
+ import net.mullvad.talpid.util.IpUtils
19
35
import net.mullvad.talpid.util.RawNetworkState
20
36
import net.mullvad.talpid.util.defaultRawNetworkStateFlow
21
- import net.mullvad.talpid.util.hasInternetConnectivity
22
37
23
- class ConnectivityListener (private val connectivityManager : ConnectivityManager ) {
24
- private lateinit var _isConnected : StateFlow <Boolean >
38
+ class ConnectivityListener (
39
+ private val connectivityManager : ConnectivityManager ,
40
+ val protect : (socket: DatagramSocket ) -> Boolean ,
41
+ ) {
42
+ private lateinit var _isConnected : StateFlow <Connectivity >
25
43
// Used by JNI
26
44
val isConnected
27
45
get() = _isConnected .value
@@ -37,6 +55,7 @@ class ConnectivityListener(private val connectivityManager: ConnectivityManager)
37
55
val currentDnsServers: ArrayList <InetAddress >
38
56
get() = _mutableNetworkState .value?.dnsServers ? : ArrayList ()
39
57
58
+ @OptIn(FlowPreview ::class )
40
59
fun register (scope : CoroutineScope ) {
41
60
// Consider implementing retry logic for the flows below, because registering a listener on
42
61
// the default network may fail if the network on Android 11
@@ -53,15 +72,59 @@ class ConnectivityListener(private val connectivityManager: ConnectivityManager)
53
72
54
73
_isConnected =
55
74
connectivityManager
56
- .hasInternetConnectivity()
57
- .onEach { notifyConnectivityChange(it) }
75
+ .defaultRawNetworkStateFlow()
76
+ .debounce(300 .milliseconds)
77
+ .map { it.toConnectivity() }
78
+ .distinctUntilChanged()
79
+ .onEach { notifyConnectivityChange(it.ipv4, it.ipv6) }
58
80
.stateIn(
59
- scope,
81
+ scope + Dispatchers . IO ,
60
82
SharingStarted .Eagerly ,
61
- true , // Assume we have internet until we know otherwise
83
+ // Has to happen on IO to avoid NetworkOnMainThreadException, we actually don't
84
+ // send any traffic just open a socket to detect the IP version.
85
+ runBlocking(Dispatchers .IO ) {
86
+ connectivityManager.activeRawNetworkState().toConnectivity()
87
+ },
62
88
)
63
89
}
64
90
91
+ private fun ConnectivityManager.activeRawNetworkState (): RawNetworkState ? =
92
+ try {
93
+ activeNetwork?.let { initialNetwork: Network ->
94
+ RawNetworkState (
95
+ network = initialNetwork,
96
+ linkProperties = getLinkProperties(initialNetwork),
97
+ networkCapabilities = getNetworkCapabilities(initialNetwork),
98
+ )
99
+ }
100
+ } catch (_: RuntimeException ) {
101
+ Logger .e(
102
+ " Unable to get active network or properties and capabilities of the active network"
103
+ )
104
+ null
105
+ }
106
+
107
+ private fun RawNetworkState?.toConnectivity (): Connectivity .Status =
108
+ if (isVpn()) {
109
+ // If the default network is a VPN we need to use a socket to check
110
+ // the underlying network
111
+ Connectivity .Status (
112
+ IpUtils .hasIPv4(protect = { protect(it) }),
113
+ IpUtils .hasIPv6(protect = { protect(it) }),
114
+ )
115
+ } else {
116
+ // If the default network is not a VPN we can check the addresses
117
+ // directly
118
+ Connectivity .Status (
119
+ ipv4 =
120
+ this ?.linkProperties?.routes?.any { it.destination.address is Inet4Address } ==
121
+ true ,
122
+ ipv6 =
123
+ this ?.linkProperties?.routes?.any { it.destination.address is Inet6Address } ==
124
+ true ,
125
+ )
126
+ }
127
+
65
128
/* *
66
129
* Invalidates the network state cache. E.g when the VPN is connected or disconnected, and we
67
130
* know the last known values not to be correct anymore.
@@ -73,14 +136,18 @@ class ConnectivityListener(private val connectivityManager: ConnectivityManager)
73
136
private fun LinkProperties.dnsServersWithoutFallback (): List <InetAddress > =
74
137
dnsServers.filter { it.hostAddress != TalpidVpnService .FALLBACK_DUMMY_DNS_SERVER }
75
138
139
+ private fun RawNetworkState?.isVpn (): Boolean =
140
+ this ?.networkCapabilities?.hasCapability(NetworkCapabilities .NET_CAPABILITY_NOT_VPN ) ==
141
+ false
142
+
76
143
private fun RawNetworkState.toNetworkState (): NetworkState =
77
144
NetworkState (
78
145
network.networkHandle,
79
146
linkProperties?.routes,
80
147
linkProperties?.dnsServersWithoutFallback(),
81
148
)
82
149
83
- private external fun notifyConnectivityChange (isConnected : Boolean )
150
+ private external fun notifyConnectivityChange (isIPv4 : Boolean , isIPv6 : Boolean )
84
151
85
152
private external fun notifyDefaultNetworkChange (networkState : NetworkState ? )
86
153
}
0 commit comments