Skip to content

Commit e945bfa

Browse files
Refactor the callback into separate delegate and add tests
1 parent c010b50 commit e945bfa

File tree

6 files changed

+911
-199
lines changed

6 files changed

+911
-199
lines changed
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
/*
2+
* Copyright (c) 2014-2025 Stream.io Inc. All rights reserved.
3+
*
4+
* Licensed under the Stream License;
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://github.com/GetStream/stream-core-android/blob/main/LICENSE
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.getstream.android.core.internal.observers.network
17+
18+
import android.net.ConnectivityManager
19+
import android.net.LinkProperties
20+
import android.net.Network
21+
import android.net.NetworkCapabilities
22+
import android.os.Build
23+
import io.getstream.android.core.api.log.StreamLogger
24+
import io.getstream.android.core.api.model.connection.network.StreamNetworkInfo
25+
import io.getstream.android.core.api.observers.network.StreamNetworkMonitorListener
26+
import io.getstream.android.core.api.subscribe.StreamSubscriptionManager
27+
import java.util.concurrent.atomic.AtomicReference
28+
import kotlinx.coroutines.CoroutineScope
29+
import kotlinx.coroutines.launch
30+
31+
internal class StreamNetworkMonitorCallback(
32+
private val logger: StreamLogger,
33+
private val scope: CoroutineScope,
34+
private val subscriptionManager: StreamSubscriptionManager<StreamNetworkMonitorListener>,
35+
private val snapshotBuilder: StreamNetworkSnapshotBuilder,
36+
private val connectivityManager: ConnectivityManager,
37+
) : ConnectivityManager.NetworkCallback() {
38+
39+
private val activeState = AtomicReference<ActiveNetworkState?>()
40+
41+
fun onRegistered() {
42+
val initialState = resolveInitialState()
43+
if (initialState != null) {
44+
activeState.set(initialState)
45+
notifyConnected(initialState.snapshot)
46+
}
47+
}
48+
49+
fun onCleared() {
50+
activeState.set(null)
51+
}
52+
53+
override fun onAvailable(network: Network) {
54+
logger.v { "Network available: $network" }
55+
handleUpdate(network, null, null, UpdateReason.AVAILABLE)
56+
}
57+
58+
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
59+
logger.v { "Network capabilities changed for $network" }
60+
handleUpdate(network, networkCapabilities, null, UpdateReason.PROPERTIES)
61+
}
62+
63+
override fun onLinkPropertiesChanged(network: Network, linkProperties: LinkProperties) {
64+
logger.v { "Link properties changed for $network" }
65+
handleUpdate(network, null, linkProperties, UpdateReason.PROPERTIES)
66+
}
67+
68+
override fun onLost(network: Network) {
69+
handleLoss(network, permanent = false)
70+
}
71+
72+
override fun onUnavailable() {
73+
handleLoss(network = null, permanent = true)
74+
}
75+
76+
private fun resolveInitialState(): ActiveNetworkState? {
77+
val defaultNetwork = resolveDefaultNetwork() ?: run {
78+
logger.v { "No active network available at start" }
79+
return null
80+
}
81+
val capabilities = connectivityManager.getNetworkCapabilities(defaultNetwork)
82+
val linkProperties = connectivityManager.getLinkProperties(defaultNetwork)
83+
val snapshot = buildSnapshot(defaultNetwork, capabilities, linkProperties) ?: return null
84+
return ActiveNetworkState(defaultNetwork, capabilities, linkProperties, snapshot)
85+
}
86+
87+
private fun resolveDefaultNetwork(): Network? =
88+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
89+
connectivityManager.activeNetwork ?: connectivityManager.allNetworks.firstOrNull()
90+
} else {
91+
connectivityManager.allNetworks.firstOrNull()
92+
}
93+
94+
private fun buildSnapshot(
95+
network: Network,
96+
capabilities: NetworkCapabilities?,
97+
linkProperties: LinkProperties?,
98+
): StreamNetworkInfo.Snapshot? =
99+
snapshotBuilder
100+
.build(network, capabilities, linkProperties)
101+
.getOrElse { throwable ->
102+
logger.e(throwable) { "Failed to assemble network snapshot" }
103+
null
104+
}
105+
106+
private fun handleUpdate(
107+
network: Network,
108+
capabilities: NetworkCapabilities?,
109+
linkProperties: LinkProperties?,
110+
reason: UpdateReason,
111+
) {
112+
if (!shouldProcessNetwork(network)) {
113+
logger.v { "[handleUpdate] Ignoring network $network; not default." }
114+
return
115+
}
116+
117+
val resolvedCapabilities =
118+
capabilities ?: connectivityManager.getNetworkCapabilities(network)
119+
val resolvedLink = linkProperties ?: connectivityManager.getLinkProperties(network)
120+
val snapshot = buildSnapshot(network, resolvedCapabilities, resolvedLink)
121+
if (snapshot == null) {
122+
logger.v { "[handleUpdate] Snapshot unavailable; skipping notification." }
123+
return
124+
}
125+
126+
val newState = ActiveNetworkState(network, resolvedCapabilities, resolvedLink, snapshot)
127+
val previousState = activeState.getAndSet(newState)
128+
129+
val networkChanged = previousState?.network != network || previousState == null
130+
val snapshotChanged = previousState?.snapshot != snapshot
131+
132+
when {
133+
reason == UpdateReason.AVAILABLE || networkChanged -> {
134+
logger.v { "[handleUpdate] Active network set to $network" }
135+
notifyConnected(snapshot)
136+
}
137+
138+
snapshotChanged -> {
139+
logger.v { "[handleUpdate] Network properties updated for $network" }
140+
notifyPropertiesChanged(snapshot)
141+
}
142+
143+
else -> logger.v { "[handleUpdate] No meaningful changes detected for $network" }
144+
}
145+
}
146+
147+
private fun handleLoss(network: Network?, permanent: Boolean) {
148+
val current = activeState.get()
149+
if (current == null) {
150+
logger.v { "[handleLoss] No active network to clear." }
151+
return
152+
}
153+
154+
if (network != null && network != current.network) {
155+
logger.v { "[handleLoss] Ignoring loss for non-active network: $network" }
156+
return
157+
}
158+
159+
if (activeState.compareAndSet(current, null)) {
160+
logger.v { "[handleLoss] Network lost: ${current.network}" }
161+
notifyLost(permanent)
162+
}
163+
}
164+
165+
private fun shouldProcessNetwork(network: Network): Boolean {
166+
val tracked = activeState.get()?.network
167+
if (tracked != null && tracked == network) {
168+
return true
169+
}
170+
return isDefaultNetwork(network)
171+
}
172+
173+
private fun isDefaultNetwork(network: Network): Boolean =
174+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
175+
connectivityManager.activeNetwork == network
176+
} else {
177+
connectivityManager.allNetworks.firstOrNull() == network
178+
}
179+
180+
private fun notifyConnected(snapshot: StreamNetworkInfo.Snapshot?) {
181+
if (snapshot == null) {
182+
return
183+
}
184+
notifyListeners { listener -> listener.onNetworkConnected(snapshot) }
185+
}
186+
187+
private fun notifyPropertiesChanged(snapshot: StreamNetworkInfo.Snapshot) {
188+
notifyListeners { listener -> listener.onNetworkPropertiesChanged(snapshot) }
189+
}
190+
191+
private fun notifyLost(permanent: Boolean) {
192+
notifyListeners { listener -> listener.onNetworkLost(permanent) }
193+
}
194+
195+
private fun notifyListeners(block: suspend (StreamNetworkMonitorListener) -> Unit) {
196+
subscriptionManager
197+
.forEach { listener ->
198+
scope.launch {
199+
runCatching { block(listener) }
200+
.onFailure { throwable ->
201+
logger.e(throwable) { "Network monitor listener failure" }
202+
}
203+
}
204+
}
205+
.onFailure { throwable ->
206+
logger.e(throwable) { "Failed to iterate network monitor listeners" }
207+
}
208+
}
209+
210+
private data class ActiveNetworkState(
211+
val network: Network,
212+
val capabilities: NetworkCapabilities?,
213+
val linkProperties: LinkProperties?,
214+
val snapshot: StreamNetworkInfo.Snapshot,
215+
)
216+
217+
private enum class UpdateReason {
218+
AVAILABLE,
219+
PROPERTIES,
220+
}
221+
}

0 commit comments

Comments
 (0)