android / nowinandroid

A fully functional Android app built entirely with Kotlin and Jetpack Compose
Apache License 2.0
15.94k stars 2.82k forks source link

[Bug]: ConnectivityManagerNetworkMonitor: onBlockedStatusChanged(...) #904

Open yikeng opened 10 months ago

yikeng commented 10 months ago

Is there an existing issue for this?

Is there a StackOverflow question about this issue?

What happened?

When an app goes to the background, onBlockedStatusChanged is called with true and then called with false when it comes back to the foreground. The function onAvailable isn't called in this case.

When blocked = true, The app cannot access the network; When blocked = false, The app can access the network.

In this scenario, NetworkMonitor.isOnline is not changed!

code :

` class ConnectivityManagerNetworkMonitor @Inject constructor( @ApplicationContext private val context: Context, ) : NetworkMonitor { override val isOnline: Flow = 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<Network>()

        override fun onAvailable(network: Network) {
            networks += network
            channel.trySend(true)
            Timber.i("onAvailable:${networks.isNotEmpty()}")
        }

        override fun onLost(network: Network) {
            networks -= network
            channel.trySend(networks.isNotEmpty())
            Timber.i("onLost:${networks.isNotEmpty()}")
        }

        override fun onCapabilitiesChanged(
            network: Network,
            networkCapabilities: NetworkCapabilities
        ) {
            Timber.i("onCapabilitiesChanged:${network}, ${networkCapabilities.toString()}")
        }

        override fun onBlockedStatusChanged(network: Network, blocked: Boolean) {
            Timber.i("onCapabilitiesChanged:${network}, blocked = $blocked")
        }
    }

    val request = Builder()
        .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
        .build()
    connectivityManager.registerNetworkCallback(request, callback)

    /**
     * Sends the latest connectivity status to the underlying channel.
     */
    channel.trySend(connectivityManager.isCurrentlyConnected())

    awaitClose {
        connectivityManager.unregisterNetworkCallback(callback)
    }
}
    .conflate()

}

`

Relevant logcat output

No response

Code of Conduct

Alamingazipur commented 10 months ago

Is there an existing issue for this?

  • [X] I have searched the existing issues

Is there a StackOverflow question about this issue?

  • [X] I have searched StackOverflow

What happened?

When an app goes to the background, onBlockedStatusChanged is called with true and then called with false when it comes back to the foreground. The function onAvailable isn't called in this case.

When blocked = true, The app cannot access the network; When blocked = false, The app can access the network.

In this scenario, NetworkMonitor.isOnline is not changed!

code :

` class ConnectivityManagerNetworkMonitor @Inject constructor( @ApplicationContext private val context: Context, ) : NetworkMonitor { override val isOnline: Flow = 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<Network>()

        override fun onAvailable(network: Network) {
            networks += network
            channel.trySend(true)
            Timber.i("onAvailable:${networks.isNotEmpty()}")
        }

        override fun onLost(network: Network) {
            networks -= network
            channel.trySend(networks.isNotEmpty())
            Timber.i("onLost:${networks.isNotEmpty()}")
        }

        override fun onCapabilitiesChanged(
            network: Network,
            networkCapabilities: NetworkCapabilities
        ) {
            Timber.i("onCapabilitiesChanged:${network}, ${networkCapabilities.toString()}")
        }

        override fun onBlockedStatusChanged(network: Network, blocked: Boolean) {
            Timber.i("onCapabilitiesChanged:${network}, blocked = $blocked")
        }
    }

    val request = Builder()
        .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
        .build()
    connectivityManager.registerNetworkCallback(request, callback)

    /**
     * Sends the latest connectivity status to the underlying channel.
     */
    channel.trySend(connectivityManager.isCurrentlyConnected())

    awaitClose {
        connectivityManager.unregisterNetworkCallback(callback)
    }
}
    .conflate()

}

`

Relevant logcat output

No response

Code of Conduct

  • [X] I agree to follow this project's Code of Conduct
Alamingazipur commented 10 months ago

Is there an existing issue for this?

  • [X] I have searched the existing issues

Is there a StackOverflow question about this issue?

  • [X] I have searched StackOverflow

What happened?

When an app goes to the background, onBlockedStatusChanged is called with true and then called with false when it comes back to the foreground. The function onAvailable isn't called in this case.

When blocked = true, The app cannot access the network; When blocked = false, The app can access the network.

In this scenario, NetworkMonitor.isOnline is not changed!

code :

` class ConnectivityManagerNetworkMonitor @Inject constructor( @ApplicationContext private val context: Context, ) : NetworkMonitor { override val isOnline: Flow = 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<Network>()

        override fun onAvailable(network: Network) {
            networks += network
            channel.trySend(true)
            Timber.i("onAvailable:${networks.isNotEmpty()}")
        }

        override fun onLost(network: Network) {
            networks -= network
            channel.trySend(networks.isNotEmpty())
            Timber.i("onLost:${networks.isNotEmpty()}")
        }

        override fun onCapabilitiesChanged(
            network: Network,
            networkCapabilities: NetworkCapabilities
        ) {
            Timber.i("onCapabilitiesChanged:${network}, ${networkCapabilities.toString()}")
        }

        override fun onBlockedStatusChanged(network: Network, blocked: Boolean) {
            Timber.i("onCapabilitiesChanged:${network}, blocked = $blocked")
        }
    }

    val request = Builder()
        .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
        .build()
    connectivityManager.registerNetworkCallback(request, callback)

    /**
     * Sends the latest connectivity status to the underlying channel.
     */
    channel.trySend(connectivityManager.isCurrentlyConnected())

    awaitClose {
        connectivityManager.unregisterNetworkCallback(callback)
    }
}
    .conflate()

}

`

Relevant logcat output

No response

Code of Conduct

  • [X] I agree to follow this project's Code of Conduct
SimonMarquis commented 10 months ago

Indeed, I've been able to reproduce this by configuring the device into forced idle mode:

adb shell dumpsys deviceidle force-idle

Then, locking the device or going to background will trigger onBlockedStatusChanged(blocked=true). And finally, resuming the app will result in onAvailable() (not always) and onBlockedStatusChanged(blocked=false).

I'm not 100% sure what a "blocked" network implies: does it put the network calls in standby, or are they immediately stopped? If we were to support this, I suppose this simple code could be added to the NetworkCallback:

override fun onBlockedStatusChanged(network: Network, blocked: Boolean) {
    if (blocked) networks -= network else networks += network
    channel.trySend(networks.isNotEmpty())
}
yikeng commented 10 months ago

Indeed, I've been able to reproduce this by configuring the device into forced idle mode:

adb shell dumpsys deviceidle force-idle

Then, locking the device or going to background will trigger onBlockedStatusChanged(blocked=true). And finally, resuming the app will result in onAvailable() (not always) and onBlockedStatusChanged(blocked=false).

I'm not 100% sure what a "blocked" network implies: does it put the network calls in standby, or are they immediately stopped? If we were to support this, I suppose this simple code could be added to the NetworkCallback:

override fun onBlockedStatusChanged(network: Network, blocked: Boolean) {
    if (blocked) networks -= network else networks += network
    channel.trySend(networks.isNotEmpty())
}

Thank you!

I think the "blocked" network means the network are immediately stopped, because the tcp connection(in my app) was immediately disconnected.