Search code examples
androidandroid-jetpack-composeconnectivityandroid-connectivitymanager

Compose TooManyRequestsException when this is not more than 1 callback registered


I used this tutorial to implement internet connection observing in my fully Compose app

Here is my code:

sealed class ConnectionState {
    data object Available : ConnectionState()
    data object Unavailable : ConnectionState()
}

val Context.isConnected: ConnectionState
    get() {
        val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
        val network = connectivityManager.activeNetwork ?: return ConnectionState.Unavailable
        val activeNetwork = connectivityManager.getNetworkCapabilities(network) ?: return ConnectionState.Unavailable

        return when {
            activeNetwork.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> ConnectionState.Available
            activeNetwork.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> ConnectionState.Available
            else -> ConnectionState.Unavailable
        }
    }

/**
 * observe availability or unavailability of Internet connection
 */
fun Context.observeConnectivityAsFlow() = callbackFlow {
    Log.d("Bazinga", "callbackFlow")
    val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager

    val callback = NetworkCallback { connectionState -> trySend(connectionState) }

    val networkRequest = NetworkRequest.Builder()
        .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
        .build()

    val registered = mutableListOf<NetworkCallback>()
    Log.d("Bazinga", "register - $callback")
    registered.add(callback)
    Log.d("Bazinga", "registered: $registered")
    connectivityManager.registerNetworkCallback(networkRequest, callback)

    trySend(isConnected)

    awaitClose {
        Log.d("Bazinga", "unregister - $callback")
        registered.remove(callback)
        connectivityManager.unregisterNetworkCallback(callback)
    }
}

fun NetworkCallback(callback: (ConnectionState) -> Unit): ConnectivityManager.NetworkCallback {
    return object : ConnectivityManager.NetworkCallback() {
        override fun onAvailable(network: Network) {
            callback(ConnectionState.Available)
        }

        override fun onLost(network: Network) {
            callback(ConnectionState.Unavailable)
        }
    }
}

@ExperimentalCoroutinesApi
@Composable
fun connectivityState(): State<ConnectionState> {
    val context = LocalContext.current
    return produceState(initialValue = context.isConnected) {
        Log.d("Bazinga", "produceState")
        context.observeConnectivityAsFlow().collect { value = it }
    }
}

@Composable
fun ObserveConnection(onConnectionResumed: () -> Unit) {
    val connection by connectivityState()
    LaunchedEffect(key1 = Unit) {
        snapshotFlow { connection }
            .collect {
                if (it == ConnectionState.Available) onConnectionResumed()
            }
    }
}

On some screens where I need to do something when internet connection was restored I use this code:

ObserveConnection(viewModel::onConnectionResumed)

And the most strange part as for me, when internet connected here is no any crashes. But in other hand, when with these steps:

  1. enable internet
  2. go to Screen A (with calling ObserveConnection(viewModel::onConnectionResumed))
  3. go to Screen B (without additional observe logic)
  4. disable internet
  5. press system back button (returns to Screen A)
  6. go to Screen B again
  7. press system back button
  8. CRASH!

As you see, I was added logs to see all register and unregister events, here is the logcat output in this case:

produceState
callbackFlow
register - com.mypackagename.utils.network.NetworkUtilsKt$NetworkCallback$1@e8f1a28
registered: [com.mypackagename.utils.network.NetworkUtilsKt$NetworkCallback$1@e8f1a28]
unregister - com.mypackagename.utils.network.NetworkUtilsKt$NetworkCallback$1@e8f1a28
produceState
callbackFlow
register - com.mypackagename.utils.network.NetworkUtilsKt$NetworkCallback$1@232649d
registered: [com.mypackagename.utils.network.NetworkUtilsKt$NetworkCallback$1@232649d]
unregister - com.mypackagename.utils.network.NetworkUtilsKt$NetworkCallback$1@232649d
produceState
callbackFlow
register - com.mypackagename.utils.network.NetworkUtilsKt$NetworkCallback$1@3636fc
registered: [com.mypackagename.utils.network.NetworkUtilsKt$NetworkCallback$1@3636fc]

Here is the error stacktrace:

FATAL EXCEPTION: main
                 Process: com.mypackagename, PID: 12545
                 android.net.ConnectivityManager$TooManyRequestsException
                    at android.net.ConnectivityManager.convertServiceException(ConnectivityManager.java:4032)
                    at android.net.ConnectivityManager.sendRequestForNetwork(ConnectivityManager.java:4221)
                    at android.net.ConnectivityManager.sendRequestForNetwork(ConnectivityManager.java:4228)
                    at android.net.ConnectivityManager.registerNetworkCallback(ConnectivityManager.java:4610)
                    at android.net.ConnectivityManager.registerNetworkCallback(ConnectivityManager.java:4580)
                    at com.mypackagename.utils.network.NetworkUtilsKt$observeConnectivityAsFlow$1.invokeSuspend(NetworkUtils.kt:54)
                    at com.mypackagename.utils.network.NetworkUtilsKt$observeConnectivityAsFlow$1.invoke(Unknown Source:8)
                    at com.mypackagename.utils.network.NetworkUtilsKt$observeConnectivityAsFlow$1.invoke(Unknown Source:4)
                    at kotlinx.coroutines.flow.ChannelFlowBuilder.collectTo$suspendImpl(Builders.kt:320)
                    at kotlinx.coroutines.flow.ChannelFlowBuilder.collectTo(Unknown Source:0)
                    at kotlinx.coroutines.flow.CallbackFlowBuilder.collectTo(Builders.kt:334)
                    at kotlinx.coroutines.flow.internal.ChannelFlow$collectToFun$1.invokeSuspend(ChannelFlow.kt:60)
                    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
                    at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:108)
                    at androidx.compose.ui.platform.AndroidUiDispatcher.performTrampolineDispatch(AndroidUiDispatcher.android.kt:81)
                    at androidx.compose.ui.platform.AndroidUiDispatcher.access$performTrampolineDispatch(AndroidUiDispatcher.android.kt:41)
                    at androidx.compose.ui.platform.AndroidUiDispatcher$dispatchCallback$1.run(AndroidUiDispatcher.android.kt:57)
                    at android.os.Handler.handleCallback(Handler.java:942)
                    at android.os.Handler.dispatchMessage(Handler.java:99)
                    at android.os.Looper.loopOnce(Looper.java:201)
                    at android.os.Looper.loop(Looper.java:288)
                    at android.app.ActivityThread.main(ActivityThread.java:7872)
                    at java.lang.reflect.Method.invoke(Native Method)
                    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
                    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)
                    Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [androidx.compose.ui.platform.MotionDurationScaleImpl@8fdf886, androidx.compose.runtime.BroadcastFrameClock@37fd547, StandaloneCoroutine{Cancelling}@c53a174, AndroidUiDispatcher@e85469d]

Solution

  • So, here is my solution

    Firstly I created in my AppState this property:

    var connectionResumed by mutableStateOf(false)
        private set
    
    fun setInternetConnectionResumed(resumed:Boolean){
        connectionResumed = resumed
    }
    

    The AppState is class to store all common things like alert state, snackbar state etc

    Also I moved handling of connectivity changes handling to single place, in my case it's MyApp Composable function that observes appState:

    val connection by connectivityState()
    LaunchedEffect(key1 = connection) {
        if (connection == ConnectionState.Available) {
           state.setInternetConnectionResumed(true)
        }
    }
    

    Also I was changed my ObserveConnection function:

    @Composable
    fun ObserveConnection(onConnectionResumed: () -> Unit, appState: AppState) {
        LaunchedEffect(key1 = appState.connectionResumed) {
            onConnectionResumed()
            appState.setInternetConnectionResumed(false)
        }
    }
    

    And everywhere where I need to handle connection events now I place this code:

    ObserveConnection(viewModel::onConnectionResumed, appState)
    

    So now there is just one subscription to the connectivityState() and the crash doesn't happen again