@@ -6,30 +6,34 @@ import android.content.Context
6
6
import android.content.Intent
7
7
import android.content.IntentFilter
8
8
import android.net.ConnectivityManager
9
+ import android.net.ConnectivityManager.NetworkCallback
9
10
import android.net.Network
10
11
import android.net.NetworkCapabilities
12
+ import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
13
+ import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
11
14
import android.net.NetworkRequest
12
- import android.os.Build
15
+ import android.os.Build.VERSION
16
+ import android.os.Build.VERSION_CODES
17
+ import androidx.annotation.VisibleForTesting
13
18
import com.amplitude.common.Logger
14
19
15
- class AndroidNetworkListener (private val context : Context , private val logger : Logger ) {
16
- private var networkCallback: NetworkChangeCallback ? = null
20
+ class AndroidNetworkListener (
21
+ private val context : Context ,
22
+ private val logger : Logger ,
23
+ private val networkCallback : NetworkChangeCallback ,
24
+ ) {
17
25
private var networkCallbackForLowerApiLevels: BroadcastReceiver ? = null
18
- private var networkCallbackForHigherApiLevels: ConnectivityManager . NetworkCallback ? = null
26
+ private var networkCallbackForHigherApiLevels: NetworkCallback ? = null
19
27
20
28
interface NetworkChangeCallback {
21
29
fun onNetworkAvailable ()
22
30
23
31
fun onNetworkUnavailable ()
24
32
}
25
33
26
- fun setNetworkChangeCallback (callback : NetworkChangeCallback ) {
27
- this .networkCallback = callback
28
- }
29
-
30
34
fun startListening () {
31
35
try {
32
- if (Build . VERSION .SDK_INT >= Build . VERSION_CODES .LOLLIPOP ) {
36
+ if (VERSION .SDK_INT >= VERSION_CODES .LOLLIPOP ) {
33
37
setupNetworkCallback()
34
38
} else {
35
39
setupBroadcastReceiver()
@@ -48,29 +52,66 @@ class AndroidNetworkListener(private val context: Context, private val logger: L
48
52
@SuppressLint(" NewApi" , " MissingPermission" )
49
53
// startListening() checks API level
50
54
// ACCESS_NETWORK_STATE permission should be added manually by users to enable this feature
51
- private fun setupNetworkCallback () {
52
- val connectivityManager =
53
- context.getSystemService(Context .CONNECTIVITY_SERVICE ) as ConnectivityManager
54
- val networkRequest =
55
- NetworkRequest .Builder ()
56
- .addCapability(NetworkCapabilities .NET_CAPABILITY_INTERNET )
57
- .build()
58
-
59
- networkCallbackForHigherApiLevels =
60
- object : ConnectivityManager .NetworkCallback () {
61
- override fun onAvailable (network : Network ) {
62
- networkCallback?.onNetworkAvailable()
63
- }
55
+ @VisibleForTesting
56
+ internal fun setupNetworkCallback () {
57
+ val connectivityManager = context
58
+ .getSystemService(Context .CONNECTIVITY_SERVICE ) as ConnectivityManager
59
+ val networkRequest = NetworkRequest .Builder ()
60
+ .addCapability(NET_CAPABILITY_INTERNET )
61
+ .build()
64
62
65
- override fun onLost (network : Network ) {
66
- networkCallback?.onNetworkUnavailable()
67
- }
63
+ networkCallbackForHigherApiLevels = object : NetworkCallback () {
64
+ private var networkState: NetworkState ? = null
65
+
66
+ override fun onAvailable (network : Network ) {
67
+ // A default network is available, set the network state and callback
68
+ val capabilities = connectivityManager.getNetworkCapabilities(network)
69
+ networkState = NetworkState (
70
+ network = network,
71
+ networkCallback = networkCallback,
72
+ available = capabilities?.available() ? : true ,
73
+ blocked = false
74
+ )
75
+ }
76
+
77
+ override fun onUnavailable () {
78
+ // no network is found
79
+ networkCallback.onNetworkUnavailable()
68
80
}
69
81
70
- connectivityManager.registerNetworkCallback(
71
- networkRequest,
72
- networkCallbackForHigherApiLevels!!
73
- )
82
+ override fun onLost (network : Network ) {
83
+ networkState?.update(network, available = false )
84
+ }
85
+
86
+ override fun onCapabilitiesChanged (
87
+ network : Network ,
88
+ networkCapabilities : NetworkCapabilities ,
89
+ ) {
90
+ networkState?.update(network, available = networkCapabilities.available())
91
+ }
92
+
93
+ override fun onBlockedStatusChanged (
94
+ network : Network ,
95
+ blocked : Boolean ,
96
+ ) {
97
+ networkState?.update(network, blocked = blocked)
98
+ }
99
+
100
+ // Best attempt to check if the network is available
101
+ private fun NetworkCapabilities.available (): Boolean {
102
+ val validated = if (VERSION .SDK_INT >= VERSION_CODES .M ) {
103
+ hasCapability(NET_CAPABILITY_VALIDATED )
104
+ } else {
105
+ true
106
+ }
107
+ return hasCapability(NET_CAPABILITY_INTERNET ) && validated
108
+ }
109
+ }.also { callbackForHigherApiLevels ->
110
+ connectivityManager.registerNetworkCallback(
111
+ networkRequest,
112
+ callbackForHigherApiLevels
113
+ )
114
+ }
74
115
}
75
116
76
117
private fun setupBroadcastReceiver () {
@@ -82,15 +123,15 @@ class AndroidNetworkListener(private val context: Context, private val logger: L
82
123
intent : Intent ,
83
124
) {
84
125
if (ConnectivityManager .CONNECTIVITY_ACTION == intent.action) {
85
- val connectivityManager =
86
- context .getSystemService(Context .CONNECTIVITY_SERVICE ) as ConnectivityManager
126
+ val connectivityManager = context
127
+ .getSystemService(Context .CONNECTIVITY_SERVICE ) as ConnectivityManager
87
128
val activeNetwork = connectivityManager.activeNetworkInfo
88
129
val isConnected = activeNetwork?.isConnectedOrConnecting == true
89
130
90
131
if (isConnected) {
91
- networkCallback? .onNetworkAvailable()
132
+ networkCallback.onNetworkAvailable()
92
133
} else {
93
- networkCallback? .onNetworkUnavailable()
134
+ networkCallback.onNetworkUnavailable()
94
135
}
95
136
}
96
137
}
@@ -102,7 +143,7 @@ class AndroidNetworkListener(private val context: Context, private val logger: L
102
143
103
144
fun stopListening () {
104
145
try {
105
- if (Build . VERSION .SDK_INT >= Build . VERSION_CODES .LOLLIPOP ) {
146
+ if (VERSION .SDK_INT >= VERSION_CODES .LOLLIPOP ) {
106
147
val connectivityManager =
107
148
context.getSystemService(Context .CONNECTIVITY_SERVICE ) as ConnectivityManager
108
149
networkCallbackForHigherApiLevels?.let {
@@ -127,4 +168,53 @@ class AndroidNetworkListener(private val context: Context, private val logger: L
127
168
logger.warn(" Error stopping network listener: ${throwable.message} " )
128
169
}
129
170
}
171
+
172
+ /* *
173
+ * NetworkState is used to track the state of a network connection.
174
+ * It considers the availability and blocked status of the network before notifying the callback.
175
+ *
176
+ * On initialization, it checks if the network is available and not blocked.
177
+ */
178
+ private class NetworkState (
179
+ private val network : Network ,
180
+ private val networkCallback : NetworkChangeCallback ,
181
+ private var available : Boolean ,
182
+ private var blocked : Boolean ,
183
+ ) {
184
+ init {
185
+ notifyNetworkCallback()
186
+ }
187
+
188
+ /* *
189
+ * Notify the network callback based on the current state of the network.
190
+ * Ideally called only when the state changes (e.g. initialized, available or blocked toggled).
191
+ */
192
+ private fun notifyNetworkCallback () {
193
+ if (available && ! blocked) {
194
+ networkCallback.onNetworkAvailable()
195
+ } else {
196
+ networkCallback.onNetworkUnavailable()
197
+ }
198
+ }
199
+
200
+ /* *
201
+ * Update the availability/blocked state and notify the callback if necessary.
202
+ * Checks if we're on the same network, else just ignore.
203
+ */
204
+ fun update (
205
+ network : Network ,
206
+ available : Boolean = this.available,
207
+ blocked : Boolean = this.blocked,
208
+ ) {
209
+ @SuppressLint(" NewApi" )
210
+ if (this .network != network) return
211
+ val toggled = this .available != available || this .blocked != blocked
212
+ this .available = available
213
+ this .blocked = blocked
214
+
215
+ if (toggled) {
216
+ notifyNetworkCallback()
217
+ }
218
+ }
219
+ }
130
220
}
0 commit comments