Created
July 12, 2025 09:23
-
-
Save maliksaif/e4f6c5cb2f97178aab439a3d1baf8d0f to your computer and use it in GitHub Desktop.
Connection Observer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package com.compose.playground.domain.network | |
| import android.content.Context | |
| import android.net.ConnectivityManager | |
| import android.net.Network | |
| import android.net.NetworkCapabilities | |
| import androidx.core.content.getSystemService | |
| import com.compose.playground.domain.ErrorIgnoringScope | |
| import kotlinx.coroutines.channels.awaitClose | |
| import kotlinx.coroutines.channels.trySendBlocking | |
| import kotlinx.coroutines.flow.SharingStarted | |
| import kotlinx.coroutines.flow.callbackFlow | |
| import kotlinx.coroutines.flow.distinctUntilChanged | |
| import kotlinx.coroutines.flow.map | |
| import kotlinx.coroutines.flow.shareIn | |
| class ConnectivityManagerBasedNetworkConnectivityObserver( | |
| private val applicationContext: Context, | |
| ) : NetworkConnectivityObserver { | |
| private data class NetworkData( | |
| val network: Network? = null, | |
| val networkCapabilities: NetworkCapabilities? = null, | |
| val isBlocked: Boolean? = null, | |
| ) { | |
| fun isNetworkConnectedToInternet() = network != null && networkCapabilities?.areValidForInternetConnection() == true && isBlocked == false | |
| } | |
| private val connectivityManager: ConnectivityManager by lazy { applicationContext.getSystemService()!! } | |
| override val isDataConnectionAvailable by lazy { | |
| callbackFlow { | |
| val initialNetworkData = getCurrentNetworkData() | |
| // Timber.d("NetworkConnectivityObserver#initialNetworkData=$initialNetworkData") | |
| // For initial network data, received from synchronous interface of ConnectivityManager, we need to blindly send this data downstream even | |
| // if it's not complete - because in that case it is still 100% meaningful information (if we weren't able to get full info about network | |
| // synchronously, it simply means that connection is not 100% ready to provide us access to Internet). | |
| // Only for data received inside of the callback interface of ConnectionManager we need to verify if data is complete before sending it | |
| // downstream, because for callback interface it's natural that data is provided to us in parts, each part of information in its own, | |
| // separate callback call (so, if data is not yet complete it doesn't give us any meaningful information - we still expect it to be filled | |
| // soon). | |
| trySendBlocking(initialNetworkData) | |
| val networkCallback = object : ConnectivityManager.NetworkCallback() { | |
| private var currentNetworkData = initialNetworkData | |
| override fun onAvailable(network: Network) { | |
| // Timber.d("NetworkConnectivityObserver#onAvailable(network=$network) [currentNetworkData=$currentNetworkData]") | |
| // There is only 1 DEFAULT network for app at time; if the new available network is different than the old one, we need to reset | |
| // all other data. | |
| if (network != currentNetworkData.network) { | |
| currentNetworkData = currentNetworkData.copy( | |
| network = network, | |
| networkCapabilities = null, | |
| // On some phones unblocked is the default state that we won't be notified about after new network becomes available. | |
| isBlocked = false, | |
| ) | |
| } | |
| sendToDownstreamIfCurrentNetworkDataComplete() | |
| } | |
| override fun onLost(network: Network) { | |
| // Timber.d("NetworkConnectivityObserver#onLost(network=$network) [currentNetworkData=$currentNetworkData]") | |
| if (network == currentNetworkData.network) { | |
| currentNetworkData = currentNetworkData.copy( | |
| network = null, | |
| networkCapabilities = null, | |
| isBlocked = null, | |
| ) | |
| } | |
| sendToDownstreamIfCurrentNetworkDataComplete() | |
| } | |
| override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) { | |
| // Timber.d("NetworkConnectivityObserver#onCapabilitiesChanged(network=$network, networkCapabilities=$networkCapabilities) [currentNetworkData=$currentNetworkData]") | |
| if (network == currentNetworkData.network) { | |
| currentNetworkData = currentNetworkData.copy( | |
| networkCapabilities = networkCapabilities, | |
| ) | |
| } | |
| sendToDownstreamIfCurrentNetworkDataComplete() | |
| } | |
| override fun onBlockedStatusChanged(network: Network, blocked: Boolean) { | |
| // Timber.d("NetworkConnectivityObserver#onBlockedStatusChanged(network=$network, blocked=$blocked) [currentNetworkData=$currentNetworkData]") | |
| if (network == currentNetworkData.network) { | |
| currentNetworkData = currentNetworkData.copy( | |
| isBlocked = blocked, | |
| ) | |
| } | |
| sendToDownstreamIfCurrentNetworkDataComplete() | |
| } | |
| private fun sendToDownstreamIfCurrentNetworkDataComplete() = currentNetworkData.let { networkData -> | |
| if (networkData.isDataComplete()) { | |
| trySendBlocking(networkData) | |
| } | |
| } | |
| private fun NetworkData.isDataComplete() = | |
| (network == null && networkCapabilities == null && isBlocked == null) // No network available - all data empty | |
| || (network != null && networkCapabilities != null && isBlocked != null) // Network available with all data filled | |
| } | |
| // Timber.d("NetworkConnectivityObserver#registerDefaultNetworkCallback") | |
| connectivityManager.registerDefaultNetworkCallback(networkCallback) | |
| awaitClose { | |
| // Timber.d("NetworkConnectivityObserver#unregisterNetworkCallback") | |
| connectivityManager.unregisterNetworkCallback(networkCallback) | |
| } | |
| } | |
| .map { currentNetworkData -> currentNetworkData.isNetworkConnectedToInternet() } | |
| .distinctUntilChanged() | |
| .shareIn( | |
| scope = ErrorIgnoringScope(), | |
| started = SharingStarted.WhileSubscribed( | |
| // We don't want to immediately stop sharing coroutine (and thus unregister the network callback) after the last subscriber | |
| // unsubscribes; this might be only temporary state because of, for example, screen rotation etc. We prefer to wait 1 second to | |
| // unregister. | |
| stopTimeoutMillis = 1000L, | |
| // After we finally stop sharing coroutine, we also stop listening to the network changes - as a result, the replay buffer becomes | |
| // obsolete immediately. So we also want to make it expired immediately. | |
| replayExpirationMillis = 0L, | |
| ), | |
| replay = 1, | |
| ) | |
| } | |
| private fun getCurrentNetworkData() = connectivityManager.activeNetwork?.let { activeNetwork -> | |
| NetworkData( | |
| network = activeNetwork, | |
| networkCapabilities = connectivityManager.getNetworkCapabilities(activeNetwork), | |
| // The Android SDK documentation explicitly says that if activeNetwork is not null, it means it's also not blocked | |
| isBlocked = false, | |
| ) | |
| } ?: NetworkData( | |
| network = null, | |
| networkCapabilities = null, | |
| isBlocked = null, | |
| ) | |
| } | |
| private fun NetworkCapabilities.areValidForInternetConnection() = | |
| hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment