@@ -6,54 +6,110 @@ import android.net.Network
66import android.net.NetworkCapabilities
77import android.net.NetworkRequest
88import android.os.Build
9+ import android.os.Handler
10+ import android.os.Looper
911import io.getunleash.android.util.UnleashLogger
12+ import java.util.concurrent.atomic.AtomicInteger
1013
1114interface NetworkListener {
1215 fun onAvailable ()
1316 fun onLost ()
1417}
1518
16- class NetworkStatusHelper (private val context : Context ) {
19+ class NetworkStatusHelper (
20+ private val context : Context ,
21+ private val scheduleRetry : (Long , () -> Unit ) -> Unit = { delayMs, action ->
22+ Handler (Looper .getMainLooper()).postDelayed(action, delayMs)
23+ }
24+ ) {
1725 companion object {
1826 private const val TAG = " NetworkState"
27+ internal const val MAX_REGISTRATION_ATTEMPTS = 5
28+ private const val REGISTRATION_RETRY_DELAY_MS = 200L
1929 }
2030
2131 internal val networkCallbacks = mutableListOf<ConnectivityManager .NetworkCallback >()
2232
2333 private val availableNetworks = mutableSetOf<Network >()
2434
35+ private val registrationEpoch = AtomicInteger (0 )
36+
2537 fun registerNetworkListener (listener : NetworkListener ) {
26- val connectivityManager = getConnectivityManager() ? : return
27- val requestBuilder = NetworkRequest .Builder ()
28- .addCapability(NetworkCapabilities .NET_CAPABILITY_INTERNET )
29- if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .M ) {
30- requestBuilder.addCapability(NetworkCapabilities .NET_CAPABILITY_VALIDATED )
38+ val epoch = registrationEpoch.get()
39+ registerNetworkListener(listener, MAX_REGISTRATION_ATTEMPTS , epoch)
40+ }
41+
42+ private fun registerNetworkListener (
43+ listener : NetworkListener ,
44+ remainingAttempts : Int ,
45+ epoch : Int
46+ ) {
47+ if (epoch != registrationEpoch.get()) {
48+ UnleashLogger .d(TAG , " Skipping stale network registration attempt" )
49+ return
3150 }
32- val networkRequest = requestBuilder.build()
3351
34- // wrap the listener in a NetworkCallback so the listener doesn't have to know about Android specifics
35- val networkCallback = object : ConnectivityManager .NetworkCallback () {
36- override fun onAvailable (network : Network ) {
37- availableNetworks + = network
38- listener.onAvailable()
39- }
52+ val attemptNumber = MAX_REGISTRATION_ATTEMPTS - remainingAttempts + 1
53+ try {
54+ val connectivityManager = getConnectivityManager() ? : return
55+ val networkRequest = buildNetworkRequest()
4056
41- override fun onLost (network : Network ) {
42- availableNetworks - = network
43- if (availableNetworks.isEmpty()) {
44- listener.onLost()
57+ val networkCallback = buildCallback(listener)
58+ connectivityManager.registerNetworkCallback(networkRequest, networkCallback)
59+ if (epoch != registrationEpoch.get()) {
60+ UnleashLogger .d(TAG , " Registration completed for stale attempt; unregistering callback" )
61+ connectivityManager.unregisterNetworkCallback(networkCallback)
62+ return
63+ }
64+ networkCallbacks + = networkCallback
65+ } catch (securityException: SecurityException ) {
66+ if (remainingAttempts > 1 ) {
67+ UnleashLogger .w(
68+ TAG ,
69+ " registerNetworkCallback failed on attempt $attemptNumber /$MAX_REGISTRATION_ATTEMPTS ; retrying in $REGISTRATION_RETRY_DELAY_MS ms" ,
70+ securityException
71+ )
72+ if (epoch == registrationEpoch.get()) {
73+ scheduleRetry(REGISTRATION_RETRY_DELAY_MS ) {
74+ registerNetworkListener(listener, remainingAttempts - 1 , epoch)
75+ }
4576 }
77+ } else {
78+ UnleashLogger .w(
79+ TAG ,
80+ " registerNetworkCallback failed after $attemptNumber attempts; network updates disabled" ,
81+ securityException
82+ )
4683 }
4784 }
48-
49- connectivityManager.registerNetworkCallback(networkRequest, networkCallback)
50- networkCallbacks + = networkCallback
5185 }
5286
5387 fun close () {
54- val connectivityManager = getConnectivityManager() ? : return
55- networkCallbacks.forEach {
56- connectivityManager.unregisterNetworkCallback(it)
88+ val epoch = registrationEpoch.incrementAndGet()
89+ val connectivityManager = getConnectivityManager() ? : run {
90+ networkCallbacks.clear()
91+ availableNetworks.clear()
92+ return
93+ }
94+ val callbacks = networkCallbacks.toList()
95+ networkCallbacks.clear()
96+ availableNetworks.clear()
97+ callbacks.forEach { callback ->
98+ try {
99+ connectivityManager.unregisterNetworkCallback(callback)
100+ } catch (illegalArgumentException: IllegalArgumentException ) {
101+ UnleashLogger .w(
102+ TAG ,
103+ " NetworkCallback already unregistered during close (epoch=$epoch )" ,
104+ illegalArgumentException
105+ )
106+ } catch (securityException: SecurityException ) {
107+ UnleashLogger .w(
108+ TAG ,
109+ " SecurityException while unregistering NetworkCallback during close (epoch=$epoch )" ,
110+ securityException
111+ )
112+ }
57113 }
58114 }
59115
@@ -93,4 +149,28 @@ class NetworkStatusHelper(private val context: Context) {
93149 fun isAvailable (): Boolean {
94150 return ! isAirplaneModeOn() && isNetworkAvailable()
95151 }
152+
153+ private fun buildNetworkRequest (): NetworkRequest {
154+ val requestBuilder = NetworkRequest .Builder ()
155+ .addCapability(NetworkCapabilities .NET_CAPABILITY_INTERNET )
156+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .M ) {
157+ requestBuilder.addCapability(NetworkCapabilities .NET_CAPABILITY_VALIDATED )
158+ }
159+ return requestBuilder.build()
160+ }
161+
162+ private fun buildCallback (listener : NetworkListener ) =
163+ object : ConnectivityManager .NetworkCallback () {
164+ override fun onAvailable (network : Network ) {
165+ availableNetworks + = network
166+ listener.onAvailable()
167+ }
168+
169+ override fun onLost (network : Network ) {
170+ availableNetworks - = network
171+ if (availableNetworks.isEmpty()) {
172+ listener.onLost()
173+ }
174+ }
175+ }
96176}
0 commit comments