Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save maliksaif/e4f6c5cb2f97178aab439a3d1baf8d0f to your computer and use it in GitHub Desktop.

Select an option

Save maliksaif/e4f6c5cb2f97178aab439a3d1baf8d0f to your computer and use it in GitHub Desktop.
Connection Observer
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