android / android/nowinandroid
[FR]: Add NET_CAPABILITY_VALIDATED, Transport Type Check , distinctUntilChanged() to the Flow in ConnectivityManagerNetworkMonitor.kt
- Dominant language
- Kotlin
- Stars
- 21.8k
- Forks
- 4.6k
- Avg merge
- 19h 20m
- Merged PRs (30d)
- 2
Description
### Is there an existing issue for this?
- [x] I have searched the existing issues
### Describe the problem
### 1. Add `NET_CAPABILITY_VALIDATED`
Add a check for `NET_CAPABILITY_VALIDATED` for a more reliable internet connection check on newer Android versions. A network can be "connected" but not actually have internet access (e.g., captive portals).
#### Reasoning:
- `NET_CAPABILITY_INTERNET` indicates that the network is intended to provide internet access.
- `NET_CAPABILITY_VALIDATED` confirms that the device has successfully connected to the internet through this network. Using both gives a more accurate signal of a usable internet connection.
---
### 2. Add `distinctUntilChanged()` to the Flow
The flow might emit the same value consecutively (e.g., multiple networks becoming available one after another, all causing `true` to be sent). While `conflate()` helps by dropping intermediate values, `distinctUntilChanged()` will prevent collectors from being notified if the connectivity status hasn't actually changed.
#### Reasoning:
This is a small optimization that ensures downstream collectors only react when the online status actually flips from `true` to `false` or vice-versa, reducing unnecessary recompositions or data processing.
---
### 3. Add Transport Type Check
Not all validated networks are the ones we want for general internet access. A device might have a "validated" peer-to-peer Wi-Fi network or another special-purpose network that doesn't provide the broad internet access your app expects. To improve this, we can refine our `isCurrentlyConnected` function to also check for common internet-providing transport types.
#### Reasoning:
Explicitly checking for `TRANSPORT_WIFI`, `TRANSPORT_CELLULAR`, or `TRANSPORT_ETHERNET` ensures we are connected to a standard internet uplink, filtering out misleading signals from other specialized networks. This makes our connectivity check more accurate
### Describe the solution
### 1. Add `NET_CAPABILITY_VALIDATED`
#### Suggestion:
Update the `isCurrentlyConnected` function to also check for NET_CAPABILITY_VALIDATED.
``` kotlin
@SuppressLint("ObsoleteSdkInt")
@Suppress("DEPRECATION")
private fun ConnectivityManager.isCurrentlyConnected(): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val network = activeNetwork ?: return false
val capabilities = getNetworkCapabilities(network) ?: return false
return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
&& capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) // Add this line
} else {
// For older versions
return activeNetworkInfo?.isConnected ?: false
}
}
```
And similarly, update the `NetworkRequest.Builder`:
``` kotlin
// Inside the callbackFlow block
val request = Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) // Add this line
.build()
connectivityManager.registerNetworkCallback(request, callback)
```
---
### 2. Add `distinctUntilChanged()` to the Flow
#### Suggestion:
Chain `distinctUntilChanged()` to the flow.
``` kotlin
// In ConnectivityManagerNetworkMonitor.kt
import kotlinx.coroutines.flow.distinctUntilChanged
// ... other imports
override val isOnline: Flow = callbackFlow {
// ... same implementation
}
.flowOn(ioDispatcher)
.conflate()
.distinctUntilChanged() // Add this line
```
---
### 3. Add Transport Type Check
#### Suggestion:
``` kotlin
@SuppressLint("ObsoleteSdkInt")
@Suppress("DEPRECATION")
private fun ConnectivityManager.isCurrentlyConnected(): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val network = activeNetwork ?: return false
val capabilities = getNetworkCapabilities(network) ?: return false
return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
&& capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
// Add this check for transport types
&& (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET))
} else {
// For older versions
return activeNetworkInfo?.isConnected ?: false
}
}
```
---
## Final Code after suggestions:
``` kotlin
// File: nowinandroid/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/util/ConnectivityManagerNetworkMonitor.kt
// Github file link: https://github.com/android/nowinandroid/blob/705fd9068bb2225cb351a0c10b2154e0e56f40de/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/util/ConnectivityManagerNetworkMonitor.kt
internal class ConnectivityManagerNetworkMonitor @Inject constructor(
@ApplicationContext private val context: Context,
@Dispatcher(AppDispatchers.IO) private val ioDispatcher: CoroutineDispatcher,
) : NetworkMonitor {
override val isOnline: Flow = callbackFlow {
trace("NetworkMonitor.callbackFlow") {
val connectivityManager = context.getSystemService()
if (connectivityManager == null) {
channel.trySend(false)
channel.close()
return@callbackFlow
}
/**
* The callback's methods are invoked on changes to *any* network matching the [NetworkRequest],
* not just the active network. So we can simply track the presence (or absence) of such [Network].
*/
val callback = object : NetworkCallback() {
private val networks = mutableSetOf()
override fun onAvailable(network: Network) {
networks += network
channel.trySend(true)
}
override fun onLost(network: Network) {
networks -= network
channel.trySend(networks.isNotEmpty())
}
}
trace("NetworkMonitor.registerNetworkCallback") {
val request = Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
.build()
connectivityManager.registerNetworkCallback(request, callback)
}
/**
* Sends the latest connectivity status to the underlying channel.
*/
channel.trySend(connectivityManager.isCurrentlyConnected())
awaitClose {
connectivityManager.unregisterNetworkCallback(callback)
}
}
}
.flowOn(ioDispatcher)
.conflate()
.distinctUntilChanged()
@SuppressLint("ObsoleteSdkInt")
@Suppress("DEPRECATION")
private fun ConnectivityManager.isCurrentlyConnected(): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val network = activeNetwork ?: return false
val capabilities = getNetworkCapabilities(network) ?: return false
return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
&& capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
&& (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET))
} else {
// For older versions
return activeNetworkInfo?.isConnected ?: false
}
}
}
```
### Additional context
#### Additional context
1. For Add `NET_CAPABILITY_VALIDATED` :
- [https://developer.android.com/develop/connectivity/network-ops/reading-network-state](https://developer.android.com/develop/connectivity/network-ops/reading-network-state)
- [https://developer.android.com/reference/android/net/NetworkCapabilities#NET_CAPABILITY_VALIDATED](https://developer.android.com/reference/android/net/NetworkCapabilities#NET_CAPABILITY_VALIDATED)
2. For Add Transport Type Check:
- [https://medium.com/@doronkakuli/adapting-your-network-connectivity-checks-for-android-15-a-practical-guide-2b1850619294](https://medium.com/@doronkakuli/adapting-your-network-connectivity-checks-for-android-15-a-practical-guide-2b1850619294)
### Code of Conduct
- [x] I agree to follow this project's Code of Conduct
Contributor guide
Research direction
Start by reading core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/util/ConnectivityManagerNetworkMonitor.kt and trace how the callbackFlow and isCurrentlyConnected function determine online status. Done means the monitor accounts for validated standard transports and the Flow suppresses consecutive duplicate Boolean emissions without breaking older Android handling.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- kotlin
- Domain
- mobile, networking
- Issue type
- Feature
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Stale
- Clarity
- Clearly specified
- Newbie friendliness
- 45/100