Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[for discussion] Network listener #7872

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions okhttp-android/build.gradle.kts
@@ -1,3 +1,5 @@
@file:Suppress("UnstableApiUsage")

import com.vanniktech.maven.publish.JavadocJar

plugins {
Expand Down Expand Up @@ -39,6 +41,10 @@ android {
}
}

tasks.withType<Test> {
useJUnit()
}

dependencies {
api(libs.squareup.okio)
api(projects.okhttp)
Expand All @@ -48,6 +54,7 @@ dependencies {
debugImplementation(libs.findbugs.jsr305)
compileOnly(libs.animalsniffer.annotations)
compileOnly(libs.robolectric.android)
implementation("androidx.core:core-ktx:1.10.1")

testImplementation(libs.junit)
testImplementation(libs.junit.ktx)
Expand Down
3 changes: 2 additions & 1 deletion okhttp-android/src/main/AndroidManifest.xml
@@ -1,6 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="okhttp.android.test">

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.INTERNET" />

</manifest>
@@ -0,0 +1,125 @@
/*
* Copyright 2023 Coil Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package okhttp3.android.network

import android.Manifest.permission.ACCESS_NETWORK_STATE
import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
import android.net.NetworkRequest
import androidx.annotation.MainThread
import androidx.core.content.ContextCompat
import androidx.core.content.PermissionChecker
import okhttp3.android.network.NetworkObserver.Listener
import okhttp3.internal.platform.Platform

/** Create a new [NetworkObserver]. */
internal fun NetworkObserver(
context: Context,
listener: Listener,
): NetworkObserver {
val connectivityManager: ConnectivityManager? =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager?
if (connectivityManager == null || !context.isPermissionGranted(ACCESS_NETWORK_STATE)) {
Platform.get().log("Unable to register network observer.")
return EmptyNetworkObserver()
}

return try {
RealNetworkObserver(connectivityManager, listener)
} catch (e: Exception) {
Platform.get().log("Failed to register network observer.", Platform.WARN, t = e)
EmptyNetworkObserver()
}
}

internal fun Context.isPermissionGranted(permission: String): Boolean {
return ContextCompat.checkSelfPermission(this, permission) == PermissionChecker.PERMISSION_GRANTED
}

/**
* Observes the device's network state and calls [Listener] if any state changes occur.
*
* This class provides a raw stream of updates from the network APIs. The [Listener] can be
* called multiple times for the same network state.
*/
internal interface NetworkObserver {

/** Synchronously checks if the device is online. */
val isOnline: Boolean

/** Stop observing network changes. */
fun shutdown()

/** Calls [onConnectivityChange] when a connectivity change event occurs. */
fun interface Listener {

@MainThread
fun onConnectivityChange(isOnline: Boolean)
}
}

internal class EmptyNetworkObserver : NetworkObserver {

override val isOnline get() = true

override fun shutdown() {}
}

@Suppress("DEPRECATION") // TODO: Remove uses of 'allNetworks'.
private class RealNetworkObserver(
private val connectivityManager: ConnectivityManager,
private val listener: Listener
) : NetworkObserver {

private val networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) = onConnectivityChange(network, true)
override fun onLost(network: Network) = onConnectivityChange(network, false)
}

override val isOnline: Boolean
get() = connectivityManager.allNetworks.any { it.isOnline() }

init {
val request = NetworkRequest.Builder()
.addCapability(NET_CAPABILITY_INTERNET)
.build()
connectivityManager.registerNetworkCallback(request, networkCallback)
}

override fun shutdown() {
connectivityManager.unregisterNetworkCallback(networkCallback)
}

private fun onConnectivityChange(network: Network, isOnline: Boolean) {
val isAnyOnline = connectivityManager.allNetworks.any {
if (it == network) {
// Don't trust the network capabilities for the network that just changed.
isOnline
} else {
it.isOnline()
}
}
listener.onConnectivityChange(isAnyOnline)
}

private fun Network.isOnline(): Boolean {
val capabilities = connectivityManager.getNetworkCapabilities(this)
return capabilities != null && capabilities.hasCapability(NET_CAPABILITY_INTERNET)
}
}
@@ -0,0 +1,109 @@
/*
* Copyright (c) 2022 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package okhttp3.android.network

import android.app.Application
import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkInfo
import androidx.core.content.getSystemService
import androidx.test.core.app.ApplicationProvider
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows
import org.robolectric.annotation.Config
import org.robolectric.shadows.ShadowConnectivityManager
import org.robolectric.shadows.ShadowNetwork
import org.robolectric.shadows.ShadowNetworkInfo


@RunWith(RobolectricTestRunner::class)
@Config(
sdk = [30],
)
class NetworkObserverTest {
private lateinit var connectivityShadow: ShadowConnectivityManager
private lateinit var connectivityManager: ConnectivityManager
private lateinit var networkObserver: NetworkObserver
private lateinit var context: Context

private val listener = TestNetworkListener()

var wifiNetwork: Network = ShadowNetwork.newInstance(123)
@Suppress("DEPRECATION")
var wifiNetworkInfo = ShadowNetworkInfo.newInstance(
NetworkInfo.DetailedState.CONNECTED,
ConnectivityManager.TYPE_WIFI,
0,
true,
NetworkInfo.State.CONNECTED
)

var lteNetwork: Network = ShadowNetwork.newInstance(124)
@Suppress("DEPRECATION")
var lteNetworkInfo = ShadowNetworkInfo.newInstance(
NetworkInfo.DetailedState.CONNECTED,
ConnectivityManager.TYPE_MOBILE,
0,
true,
NetworkInfo.State.CONNECTED
)

@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext<Application>()
networkObserver = NetworkObserver(context, listener)

connectivityManager = context.getSystemService<ConnectivityManager>()!!
connectivityShadow = Shadows.shadowOf(connectivityManager)
}

@Test
fun testEvents() {
connectivityShadow.clearAllNetworks()

assertThat(listener.isOnline).isFalse()

connectivityShadow.addNetwork(wifiNetwork, wifiNetworkInfo)

assertThat(listener.isOnline).isTrue()

connectivityShadow.addNetwork(lteNetwork, lteNetworkInfo)

assertThat(listener.isOnline).isTrue()

connectivityShadow.removeNetwork(wifiNetwork)

assertThat(listener.isOnline).isTrue()

connectivityShadow.removeNetwork(lteNetwork)

assertThat(listener.isOnline).isTrue()
}
}

class TestNetworkListener: NetworkObserver.Listener {
var isOnline = false

override fun onConnectivityChange(isOnline: Boolean) {
this.isOnline = isOnline
}
}