Skip to content

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

@CGreenP

Description

@CGreenP

Is there an existing issue for this?

  • 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.

@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:

// 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.

// In ConnectivityManagerNetworkMonitor.kt
import kotlinx.coroutines.flow.distinctUntilChanged
// ... other imports

override val isOnline: Flow<Boolean> = callbackFlow {
    // ... same implementation
}
    .flowOn(ioDispatcher)
    .conflate()
    .distinctUntilChanged() // Add this line

3. Add Transport Type Check

Suggestion:

@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:

// 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<Boolean> = callbackFlow {
        trace("NetworkMonitor.callbackFlow") {
            val connectivityManager = context.getSystemService<ConnectivityManager>()
            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<Network>()

                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 :
  1. For Add Transport Type Check:

Code of Conduct

  • I agree to follow this project's Code of Conduct

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions