@@ -5,8 +5,9 @@ import android.net.ConnectivityManager.NetworkCallback
5
5
import android.net.LinkProperties
6
6
import android.net.Network
7
7
import android.net.NetworkCapabilities
8
- import android.net.NetworkRequest
9
8
import co.touchlab.kermit.Logger
9
+ import java.net.Inet4Address
10
+ import java.net.Inet6Address
10
11
import kotlin.time.Duration.Companion.milliseconds
11
12
import kotlinx.coroutines.FlowPreview
12
13
import kotlinx.coroutines.channels.awaitClose
@@ -16,13 +17,12 @@ import kotlinx.coroutines.flow.callbackFlow
16
17
import kotlinx.coroutines.flow.debounce
17
18
import kotlinx.coroutines.flow.distinctUntilChanged
18
19
import kotlinx.coroutines.flow.map
19
- import kotlinx.coroutines.flow.mapNotNull
20
- import kotlinx.coroutines.flow.onStart
21
20
import kotlinx.coroutines.flow.scan
21
+ import net.mullvad.talpid.model.Connectivity
22
22
23
23
private val CONNECTIVITY_DEBOUNCE = 300 .milliseconds
24
24
25
- internal fun ConnectivityManager.defaultNetworkEvents (): Flow <NetworkEvent > = callbackFlow {
25
+ fun ConnectivityManager.defaultNetworkEvents (): Flow <NetworkEvent > = callbackFlow {
26
26
val callback =
27
27
object : NetworkCallback () {
28
28
override fun onLinkPropertiesChanged (network : Network , linkProperties : LinkProperties ) {
@@ -68,56 +68,6 @@ internal fun ConnectivityManager.defaultNetworkEvents(): Flow<NetworkEvent> = ca
68
68
awaitClose { unregisterNetworkCallback(callback) }
69
69
}
70
70
71
- fun ConnectivityManager.networkEvents (networkRequest : NetworkRequest ): Flow <NetworkEvent > =
72
- callbackFlow {
73
- val callback =
74
- object : NetworkCallback () {
75
- override fun onLinkPropertiesChanged (
76
- network : Network ,
77
- linkProperties : LinkProperties ,
78
- ) {
79
- super .onLinkPropertiesChanged(network, linkProperties)
80
- trySendBlocking(NetworkEvent .LinkPropertiesChanged (network, linkProperties))
81
- }
82
-
83
- override fun onAvailable (network : Network ) {
84
- super .onAvailable(network)
85
- trySendBlocking(NetworkEvent .Available (network))
86
- }
87
-
88
- override fun onCapabilitiesChanged (
89
- network : Network ,
90
- networkCapabilities : NetworkCapabilities ,
91
- ) {
92
- super .onCapabilitiesChanged(network, networkCapabilities)
93
- trySendBlocking(NetworkEvent .CapabilitiesChanged (network, networkCapabilities))
94
- }
95
-
96
- override fun onBlockedStatusChanged (network : Network , blocked : Boolean ) {
97
- super .onBlockedStatusChanged(network, blocked)
98
- trySendBlocking(NetworkEvent .BlockedStatusChanged (network, blocked))
99
- }
100
-
101
- override fun onLosing (network : Network , maxMsToLive : Int ) {
102
- super .onLosing(network, maxMsToLive)
103
- trySendBlocking(NetworkEvent .Losing (network, maxMsToLive))
104
- }
105
-
106
- override fun onLost (network : Network ) {
107
- super .onLost(network)
108
- trySendBlocking(NetworkEvent .Lost (network))
109
- }
110
-
111
- override fun onUnavailable () {
112
- super .onUnavailable()
113
- trySendBlocking(NetworkEvent .Unavailable )
114
- }
115
- }
116
- registerNetworkCallback(networkRequest, callback)
117
-
118
- awaitClose { unregisterNetworkCallback(callback) }
119
- }
120
-
121
71
internal fun ConnectivityManager.defaultRawNetworkStateFlow (): Flow <RawNetworkState ?> =
122
72
defaultNetworkEvents().scan(null as RawNetworkState ? ) { state, event -> state.reduce(event) }
123
73
@@ -153,74 +103,65 @@ sealed interface NetworkEvent {
153
103
data class Lost (val network : Network ) : NetworkEvent
154
104
}
155
105
156
- internal data class RawNetworkState (
106
+ data class RawNetworkState (
157
107
val network : Network ,
158
108
val linkProperties : LinkProperties ? = null ,
159
109
val networkCapabilities : NetworkCapabilities ? = null ,
160
110
val blockedStatus : Boolean = false ,
161
111
val maxMsToLive : Int? = null ,
162
112
)
163
113
164
- private val nonVPNInternetNetworksRequest =
165
- NetworkRequest .Builder ()
166
- .addCapability(NetworkCapabilities .NET_CAPABILITY_NOT_VPN )
167
- .addCapability(NetworkCapabilities .NET_CAPABILITY_INTERNET )
168
- .build()
169
-
170
- private sealed interface InternalConnectivityEvent {
171
- data class Available (val network : Network ) : InternalConnectivityEvent
172
-
173
- data class Lost (val network : Network ) : InternalConnectivityEvent
174
- }
114
+ internal fun ConnectivityManager.activeRawNetworkState (): RawNetworkState ? =
115
+ try {
116
+ activeNetwork?.let { currentNetwork: Network ->
117
+ RawNetworkState (
118
+ network = currentNetwork,
119
+ linkProperties = getLinkProperties(currentNetwork),
120
+ networkCapabilities = getNetworkCapabilities(currentNetwork),
121
+ )
122
+ }
123
+ } catch (_: RuntimeException ) {
124
+ Logger .e(
125
+ " Unable to get active network or properties and capabilities of the active network"
126
+ )
127
+ null
128
+ }
175
129
176
130
/* *
177
- * Return a flow notifying us if we have internet connectivity. Initial state will be taken from
178
- * `allNetworks` and then updated when network events occur. Important to note that `allNetworks`
179
- * may return a network that we never get updates from if turned off at the moment of the initial
180
- * query.
131
+ * Return a flow with the current internet connectivity status. The status is based on current
132
+ * default network and depending on if it is a VPN. If it is not a VPN we check the network
133
+ * properties directly and if it is a VPN we use a socket to check the underlying network. A
134
+ * debounce is applied to avoid emitting too many events and to avoid setting the app in an offline
135
+ * state when switching networks.
181
136
*/
182
137
@OptIn(FlowPreview ::class )
183
- fun ConnectivityManager.hasInternetConnectivity (): Flow <Boolean > =
184
- networkEvents(nonVPNInternetNetworksRequest)
185
- .mapNotNull {
186
- when (it) {
187
- is NetworkEvent .Available -> InternalConnectivityEvent .Available (it.network)
188
- is NetworkEvent .Lost -> InternalConnectivityEvent .Lost (it.network)
189
- else -> null
190
- }
191
- }
192
- .scan(emptySet<Network >()) { networks, event ->
193
- when (event) {
194
- is InternalConnectivityEvent .Lost -> networks - event.network
195
- is InternalConnectivityEvent .Available -> networks + event.network
196
- }.also { Logger .d(" Networks: $it " ) }
197
- }
198
- // NetworkEvents are slow, can several 100 millis to arrive. If we are online, we don't
199
- // want to emit a false offline with the initial accumulator, so we wait a bit before
200
- // emitting, and rely on `networksWithInternetConnectivity`.
201
- //
202
- // Also if our initial state was "online", but it just got turned off we might not see
203
- // any updates for this network even though we already were registered for updated, and
204
- // thus we can't drop initial value accumulator value.
138
+ fun ConnectivityManager.hasInternetConnectivity (
139
+ resolver : UnderlyingConnectivityStatusResolver
140
+ ): Flow <Connectivity .Status > =
141
+ this .defaultRawNetworkStateFlow()
205
142
.debounce(CONNECTIVITY_DEBOUNCE )
206
- .onStart {
207
- // We should not use this as initial state in scan, because it may contain networks
208
- // that won't be included in `networkEvents` updates.
209
- emit(networksWithInternetConnectivity().also { Logger .d(" Networks (Initial): $it " ) })
210
- }
211
- .map { it.isNotEmpty() }
143
+ .map { resolveConnectivityStatus(it, resolver) }
212
144
.distinctUntilChanged()
213
145
214
- @Suppress(" DEPRECATION" )
215
- fun ConnectivityManager.networksWithInternetConnectivity (): Set <Network > =
216
- // Currently the use of `allNetworks` (which is deprecated in favor of listening to network
217
- // events) is our only option because network events does not give us the initial state fast
218
- // enough.
219
- allNetworks
220
- .filter {
221
- val capabilities = getNetworkCapabilities(it) ? : return @filter false
222
-
223
- capabilities.hasCapability(NetworkCapabilities .NET_CAPABILITY_INTERNET ) &&
224
- capabilities.hasCapability(NetworkCapabilities .NET_CAPABILITY_NOT_VPN )
225
- }
226
- .toSet()
146
+ internal fun resolveConnectivityStatus (
147
+ currentRawNetworkState : RawNetworkState ? ,
148
+ resolver : UnderlyingConnectivityStatusResolver ,
149
+ ): Connectivity .Status =
150
+ if (currentRawNetworkState.isVpn()) {
151
+ // If the default network is a VPN we need to use a socket to check
152
+ // the underlying network
153
+ resolver.currentStatus()
154
+ } else {
155
+ // If the default network is not a VPN we can check the addresses
156
+ // directly
157
+ currentRawNetworkState.toConnectivityStatus()
158
+ }
159
+
160
+ private fun RawNetworkState?.toConnectivityStatus () =
161
+ Connectivity .Status (
162
+ ipv4 = this ?.linkProperties?.linkAddresses?.any { it.address is Inet4Address } == true ,
163
+ ipv6 = this ?.linkProperties?.linkAddresses?.any { it.address is Inet6Address } == true ,
164
+ )
165
+
166
+ private fun RawNetworkState?.isVpn (): Boolean =
167
+ this ?.networkCapabilities?.hasCapability(NetworkCapabilities .NET_CAPABILITY_NOT_VPN ) == false
0 commit comments