android / android/nowinandroid

[FR]: Add NET_CAPABILITY_VALIDATED, Transport Type Check , distinctUntilChanged() to the Flow in ConnectivityManagerNetworkMonitor.kt

Open
#1,982 0 comments 0 reactions 0 assignees View on GitHub
enhancement
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.