rn-network-quality 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +899 -0
  3. package/android/build.gradle +60 -0
  4. package/android/src/main/AndroidManifest.xml +3 -0
  5. package/android/src/main/java/com/rnnetworkquality/CellularInfo.kt +34 -0
  6. package/android/src/main/java/com/rnnetworkquality/DownloadPolicy.kt +26 -0
  7. package/android/src/main/java/com/rnnetworkquality/Mappers.kt +106 -0
  8. package/android/src/main/java/com/rnnetworkquality/NetworkMonitor.kt +263 -0
  9. package/android/src/main/java/com/rnnetworkquality/NetworkProbe.kt +668 -0
  10. package/android/src/main/java/com/rnnetworkquality/NetworkQualityModule.kt +287 -0
  11. package/android/src/main/java/com/rnnetworkquality/NetworkQualityPackage.kt +25 -0
  12. package/android/src/main/java/com/rnnetworkquality/NetworkSnapshot.kt +83 -0
  13. package/android/src/main/java/com/rnnetworkquality/SnapshotBuilder.kt +92 -0
  14. package/android/src/main/java/com/rnnetworkquality/Throttler.kt +100 -0
  15. package/ios/CellularInfo.swift +47 -0
  16. package/ios/NetworkProbe.swift +391 -0
  17. package/ios/NetworkQuality.h +8 -0
  18. package/ios/NetworkQuality.mm +88 -0
  19. package/ios/NetworkQualityImpl.swift +371 -0
  20. package/ios/PathSnapshot.swift +109 -0
  21. package/ios/PrivacyInfo.xcprivacy +23 -0
  22. package/ios/Throttler.swift +174 -0
  23. package/lib/module/NativeNetworkQuality.js +5 -0
  24. package/lib/module/NativeNetworkQuality.js.map +1 -0
  25. package/lib/module/classify.js +126 -0
  26. package/lib/module/classify.js.map +1 -0
  27. package/lib/module/constants.js +52 -0
  28. package/lib/module/constants.js.map +1 -0
  29. package/lib/module/errors.js +18 -0
  30. package/lib/module/errors.js.map +1 -0
  31. package/lib/module/hooks.js +78 -0
  32. package/lib/module/hooks.js.map +1 -0
  33. package/lib/module/index.js +19 -0
  34. package/lib/module/index.js.map +1 -0
  35. package/lib/module/manager.js +595 -0
  36. package/lib/module/manager.js.map +1 -0
  37. package/lib/module/normalize.js +35 -0
  38. package/lib/module/normalize.js.map +1 -0
  39. package/lib/module/package.json +1 -0
  40. package/lib/module/types.js +2 -0
  41. package/lib/module/types.js.map +1 -0
  42. package/lib/typescript/package.json +1 -0
  43. package/lib/typescript/src/NativeNetworkQuality.d.ts +50 -0
  44. package/lib/typescript/src/NativeNetworkQuality.d.ts.map +1 -0
  45. package/lib/typescript/src/classify.d.ts +12 -0
  46. package/lib/typescript/src/classify.d.ts.map +1 -0
  47. package/lib/typescript/src/constants.d.ts +15 -0
  48. package/lib/typescript/src/constants.d.ts.map +1 -0
  49. package/lib/typescript/src/errors.d.ts +13 -0
  50. package/lib/typescript/src/errors.d.ts.map +1 -0
  51. package/lib/typescript/src/hooks.d.ts +18 -0
  52. package/lib/typescript/src/hooks.d.ts.map +1 -0
  53. package/lib/typescript/src/index.d.ts +13 -0
  54. package/lib/typescript/src/index.d.ts.map +1 -0
  55. package/lib/typescript/src/manager.d.ts +98 -0
  56. package/lib/typescript/src/manager.d.ts.map +1 -0
  57. package/lib/typescript/src/normalize.d.ts +5 -0
  58. package/lib/typescript/src/normalize.d.ts.map +1 -0
  59. package/lib/typescript/src/types.d.ts +171 -0
  60. package/lib/typescript/src/types.d.ts.map +1 -0
  61. package/mock.js +318 -0
  62. package/package.json +161 -0
  63. package/rn-network-quality.podspec +32 -0
  64. package/src/NativeNetworkQuality.ts +58 -0
  65. package/src/classify.ts +208 -0
  66. package/src/constants.ts +48 -0
  67. package/src/errors.ts +23 -0
  68. package/src/hooks.ts +110 -0
  69. package/src/index.tsx +25 -0
  70. package/src/manager.ts +891 -0
  71. package/src/normalize.ts +81 -0
  72. package/src/types.ts +207 -0
@@ -0,0 +1,287 @@
1
+ package com.rnnetworkquality
2
+
3
+ import android.content.Context
4
+ import android.net.ConnectivityManager
5
+ import android.os.Handler
6
+ import android.os.HandlerThread
7
+ import android.util.Log
8
+ import com.facebook.react.bridge.Promise
9
+ import com.facebook.react.bridge.ReactApplicationContext
10
+ import com.facebook.react.bridge.ReadableMap
11
+
12
+ internal class NetworkQualityModule(
13
+ private val reactContext: ReactApplicationContext,
14
+ ) : NativeNetworkQualitySpec(reactContext) {
15
+ private val lifecycleLock = Any()
16
+ private val connectivityManager =
17
+ reactContext.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
18
+ private val snapshotBuilder = SnapshotBuilder(
19
+ connectivityManager,
20
+ CellularInfo(reactContext),
21
+ )
22
+ private val networkProbe = NetworkProbe(reactContext)
23
+
24
+ @Volatile
25
+ private var invalidated = false
26
+ private var nextGeneration = 0L
27
+ private var worker: MonitorRuntime? = null
28
+
29
+ override fun getName(): String = NAME
30
+
31
+ override fun getCurrentState(promise: Promise) {
32
+ if (invalidated) {
33
+ promise.reject(ERROR_STATE_FAILED, "NetworkQuality has been invalidated")
34
+ return
35
+ }
36
+
37
+ val runtime = synchronized(lifecycleLock) {
38
+ if (invalidated) {
39
+ null
40
+ } else {
41
+ ensureRuntimeLocked().also { it.pendingSnapshots += 1 }
42
+ }
43
+ }
44
+ if (runtime == null) {
45
+ promise.reject(ERROR_STATE_FAILED, "NetworkQuality has been invalidated")
46
+ return
47
+ }
48
+
49
+ if (!runtime.handler.post {
50
+ try {
51
+ resolveCurrentState(runtime.monitor, promise)
52
+ } finally {
53
+ completeSnapshot(runtime)
54
+ }
55
+ }
56
+ ) {
57
+ discardUnavailableRuntime(runtime)
58
+ promise.reject(ERROR_STATE_FAILED, "The network snapshot thread is unavailable")
59
+ }
60
+ }
61
+
62
+ override fun startMonitoring(options: ReadableMap) {
63
+ if (invalidated) return
64
+ val throttleMs = options.readFiniteDouble("throttleMs", DEFAULT_THROTTLE_MS).toLong()
65
+ .coerceAtLeast(0L)
66
+ val threshold = options.readFiniteDouble(
67
+ "bandwidthChangeThresholdPct",
68
+ DEFAULT_THRESHOLD_PERCENT,
69
+ ).coerceAtLeast(0.0)
70
+
71
+ val request = synchronized(lifecycleLock) {
72
+ if (invalidated) return
73
+ val runtime = ensureRuntimeLocked()
74
+ runtime.monitoring = true
75
+ runtime.commandVersion += 1
76
+ StartRequest(runtime, runtime.commandVersion)
77
+ }
78
+ if (!request.runtime.handler.post {
79
+ if (!isCurrentStart(request)) return@post
80
+ try {
81
+ if (!request.runtime.monitor.start(throttleMs, threshold)) {
82
+ completeFailedStart(request)
83
+ }
84
+ } catch (error: RuntimeException) {
85
+ Log.w(TAG, "Unable to start network monitoring", error)
86
+ try {
87
+ request.runtime.monitor.stop()
88
+ } catch (cleanupError: RuntimeException) {
89
+ Log.w(TAG, "Unable to roll back network monitoring", cleanupError)
90
+ }
91
+ completeFailedStart(request)
92
+ }
93
+ }
94
+ ) {
95
+ discardUnavailableRuntime(request.runtime)
96
+ }
97
+ }
98
+
99
+ override fun stopMonitoring() {
100
+ networkProbe.cancelActive()
101
+ val runtime = synchronized(lifecycleLock) {
102
+ val current = worker ?: return
103
+ if (!current.monitoring) return
104
+ current.monitoring = false
105
+ current.commandVersion += 1
106
+ current.stopPending = true
107
+ current
108
+ }
109
+ if (!runtime.handler.post {
110
+ try {
111
+ runtime.monitor.stop()
112
+ } catch (error: RuntimeException) {
113
+ Log.w(TAG, "Unable to stop network monitoring", error)
114
+ } finally {
115
+ completeStop(runtime)
116
+ }
117
+ }
118
+ ) {
119
+ discardUnavailableRuntime(runtime)
120
+ }
121
+ }
122
+
123
+ override fun probe(options: ReadableMap, promise: Promise) {
124
+ if (invalidated) {
125
+ promise.reject(ERROR_PROBE_FAILED, "NetworkQuality has been invalidated")
126
+ return
127
+ }
128
+ networkProbe.probe(options, promise)
129
+ }
130
+
131
+ override fun invalidate() {
132
+ val runtime = synchronized(lifecycleLock) {
133
+ if (invalidated) return
134
+ invalidated = true
135
+ worker.also { current ->
136
+ worker = null
137
+ current?.monitoring = false
138
+ current?.stopPending = true
139
+ current?.commandVersion = (current?.commandVersion ?: 0L) + 1
140
+ }
141
+ }
142
+ networkProbe.invalidate()
143
+ if (runtime != null) {
144
+ if (!runtime.handler.post {
145
+ try {
146
+ runtime.monitor.stop()
147
+ } catch (error: RuntimeException) {
148
+ Log.w(TAG, "Unable to invalidate network monitoring", error)
149
+ } finally {
150
+ runtime.thread.quitSafely()
151
+ }
152
+ }
153
+ ) {
154
+ runtime.thread.quitSafely()
155
+ }
156
+ }
157
+ super.invalidate()
158
+ }
159
+
160
+ /** Called with [lifecycleLock] held. At most one serialized handler exists at a time. */
161
+ private fun ensureRuntimeLocked(): MonitorRuntime {
162
+ worker?.let { return it }
163
+ val generation = ++nextGeneration
164
+ val thread = HandlerThread(MONITOR_THREAD_NAME).apply { start() }
165
+ val handler = Handler(thread.looper)
166
+ val monitor = NetworkMonitor(
167
+ context = reactContext,
168
+ connectivityManager = connectivityManager,
169
+ handler = handler,
170
+ snapshotBuilder = snapshotBuilder,
171
+ onSnapshot = { snapshot -> emitSnapshot(generation, snapshot) },
172
+ )
173
+ return MonitorRuntime(generation, thread, handler, monitor).also { worker = it }
174
+ }
175
+
176
+ private fun completeSnapshot(runtime: MonitorRuntime) {
177
+ synchronized(lifecycleLock) {
178
+ if (runtime.pendingSnapshots > 0) runtime.pendingSnapshots -= 1
179
+ }
180
+ releaseIfIdle(runtime)
181
+ }
182
+
183
+ private fun completeStop(runtime: MonitorRuntime) {
184
+ synchronized(lifecycleLock) {
185
+ runtime.stopPending = false
186
+ }
187
+ releaseIfIdle(runtime)
188
+ }
189
+
190
+ private fun completeFailedStart(request: StartRequest) {
191
+ synchronized(lifecycleLock) {
192
+ if (
193
+ worker === request.runtime &&
194
+ request.runtime.commandVersion == request.commandVersion
195
+ ) {
196
+ request.runtime.monitoring = false
197
+ }
198
+ }
199
+ releaseIfIdle(request.runtime)
200
+ }
201
+
202
+ private fun releaseIfIdle(runtime: MonitorRuntime) {
203
+ val shouldRelease = synchronized(lifecycleLock) {
204
+ if (
205
+ worker === runtime &&
206
+ !runtime.monitoring &&
207
+ !runtime.stopPending &&
208
+ runtime.pendingSnapshots == 0
209
+ ) {
210
+ worker = null
211
+ true
212
+ } else {
213
+ false
214
+ }
215
+ }
216
+ if (shouldRelease) runtime.thread.quitSafely()
217
+ }
218
+
219
+ private fun discardUnavailableRuntime(runtime: MonitorRuntime) {
220
+ synchronized(lifecycleLock) {
221
+ if (worker === runtime) worker = null
222
+ runtime.monitoring = false
223
+ runtime.stopPending = false
224
+ runtime.pendingSnapshots = 0
225
+ }
226
+ runtime.thread.quitSafely()
227
+ }
228
+
229
+ private fun isCurrentStart(request: StartRequest): Boolean = synchronized(lifecycleLock) {
230
+ !invalidated &&
231
+ worker === request.runtime &&
232
+ request.runtime.monitoring &&
233
+ request.runtime.commandVersion == request.commandVersion
234
+ }
235
+
236
+ private fun resolveCurrentState(monitor: NetworkMonitor, promise: Promise) {
237
+ try {
238
+ promise.resolve(monitor.readCurrentSnapshot().toWritableMap())
239
+ } catch (error: RuntimeException) {
240
+ promise.reject(ERROR_STATE_FAILED, "Unable to read the current network state", error)
241
+ }
242
+ }
243
+
244
+ private fun emitSnapshot(generation: Long, snapshot: NetworkSnapshot) {
245
+ val canEmit = synchronized(lifecycleLock) {
246
+ !invalidated && worker?.generation == generation && worker?.monitoring == true
247
+ }
248
+ if (!canEmit) return
249
+ try {
250
+ emitOnNetworkStateChange(snapshot.toWritableMap())
251
+ } catch (error: RuntimeException) {
252
+ Log.w(TAG, "Unable to emit a network state update", error)
253
+ }
254
+ }
255
+
256
+ private fun ReadableMap.readFiniteDouble(key: String, fallback: Double): Double = try {
257
+ getDouble(key).takeIf { it.isFinite() } ?: fallback
258
+ } catch (_: RuntimeException) {
259
+ fallback
260
+ }
261
+
262
+ private class MonitorRuntime(
263
+ val generation: Long,
264
+ val thread: HandlerThread,
265
+ val handler: Handler,
266
+ val monitor: NetworkMonitor,
267
+ var monitoring: Boolean = false,
268
+ var stopPending: Boolean = false,
269
+ var pendingSnapshots: Int = 0,
270
+ var commandVersion: Long = 0,
271
+ )
272
+
273
+ private data class StartRequest(
274
+ val runtime: MonitorRuntime,
275
+ val commandVersion: Long,
276
+ )
277
+
278
+ companion object {
279
+ const val NAME = "NetworkQuality"
280
+ private const val TAG = "RNNetworkQuality"
281
+ private const val MONITOR_THREAD_NAME = "RNNetworkQuality"
282
+ private const val DEFAULT_THROTTLE_MS = 1_000.0
283
+ private const val DEFAULT_THRESHOLD_PERCENT = 10.0
284
+ private const val ERROR_STATE_FAILED = "E_PROBE_FAILED"
285
+ private const val ERROR_PROBE_FAILED = "E_PROBE_FAILED"
286
+ }
287
+ }
@@ -0,0 +1,25 @@
1
+ package com.rnnetworkquality
2
+
3
+ import com.facebook.react.BaseReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfo
7
+ import com.facebook.react.module.model.ReactModuleInfoProvider
8
+
9
+ class NetworkQualityPackage : BaseReactPackage() {
10
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =
11
+ if (name == NetworkQualityModule.NAME) NetworkQualityModule(reactContext) else null
12
+
13
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider = ReactModuleInfoProvider {
14
+ mapOf(
15
+ NetworkQualityModule.NAME to ReactModuleInfo(
16
+ name = NetworkQualityModule.NAME,
17
+ className = NetworkQualityModule::class.java.name,
18
+ canOverrideExistingModule = false,
19
+ needsEagerInit = false,
20
+ isCxxModule = false,
21
+ isTurboModule = true,
22
+ )
23
+ )
24
+ }
25
+ }
@@ -0,0 +1,83 @@
1
+ package com.rnnetworkquality
2
+
3
+ import com.facebook.react.bridge.Arguments
4
+ import com.facebook.react.bridge.WritableMap
5
+
6
+ /** A complete point-in-time view of Android's default network. */
7
+ internal data class NetworkSnapshot(
8
+ val isConnected: Boolean,
9
+ val isValidated: Boolean?,
10
+ val isCaptivePortal: Boolean?,
11
+ val transport: String,
12
+ val isVpn: Boolean?,
13
+ val isExpensive: Boolean,
14
+ val isConstrained: Boolean,
15
+ val isRoaming: Boolean?,
16
+ val downlinkKbps: Int?,
17
+ val uplinkKbps: Int?,
18
+ val signalStrength: Int?,
19
+ val cellularGeneration: String?,
20
+ val supportsIPv4: Boolean?,
21
+ val supportsIPv6: Boolean?,
22
+ val supportsDNS: Boolean?,
23
+ val unsatisfiedReason: String?,
24
+ val timestamp: Long,
25
+ ) {
26
+ fun equalsIgnoringTimestamp(other: NetworkSnapshot): Boolean =
27
+ copy(timestamp = other.timestamp) == other
28
+
29
+ fun toWritableMap(): WritableMap = Arguments.createMap().apply {
30
+ putBoolean("isConnected", isConnected)
31
+ putNullableBoolean("isValidated", isValidated)
32
+ putNullableBoolean("isCaptivePortal", isCaptivePortal)
33
+ putString("transport", transport)
34
+ putNullableBoolean("isVpn", isVpn)
35
+ putBoolean("isExpensive", isExpensive)
36
+ putBoolean("isConstrained", isConstrained)
37
+ putNullableBoolean("isRoaming", isRoaming)
38
+ putNullableNumber("downlinkKbps", downlinkKbps)
39
+ putNullableNumber("uplinkKbps", uplinkKbps)
40
+ putNullableNumber("signalStrength", signalStrength)
41
+ putNullableString("cellularGeneration", cellularGeneration)
42
+ putNullableBoolean("supportsIPv4", supportsIPv4)
43
+ putNullableBoolean("supportsIPv6", supportsIPv6)
44
+ putNullableBoolean("supportsDNS", supportsDNS)
45
+ putNullableString("unsatisfiedReason", unsatisfiedReason)
46
+ putDouble("timestamp", timestamp.toDouble())
47
+ }
48
+
49
+ private fun WritableMap.putNullableBoolean(key: String, value: Boolean?) {
50
+ if (value == null) putNull(key) else putBoolean(key, value)
51
+ }
52
+
53
+ private fun WritableMap.putNullableNumber(key: String, value: Int?) {
54
+ if (value == null) putNull(key) else putDouble(key, value.toDouble())
55
+ }
56
+
57
+ private fun WritableMap.putNullableString(key: String, value: String?) {
58
+ if (value == null) putNull(key) else putString(key, value)
59
+ }
60
+
61
+ companion object {
62
+ fun disconnected(timestamp: Long, isConstrained: Boolean): NetworkSnapshot =
63
+ NetworkSnapshot(
64
+ isConnected = false,
65
+ isValidated = false,
66
+ isCaptivePortal = false,
67
+ transport = "none",
68
+ isVpn = false,
69
+ isExpensive = false,
70
+ isConstrained = isConstrained,
71
+ isRoaming = null,
72
+ downlinkKbps = null,
73
+ uplinkKbps = null,
74
+ signalStrength = null,
75
+ cellularGeneration = null,
76
+ supportsIPv4 = null,
77
+ supportsIPv6 = null,
78
+ supportsDNS = null,
79
+ unsatisfiedReason = null,
80
+ timestamp = timestamp,
81
+ )
82
+ }
83
+ }
@@ -0,0 +1,92 @@
1
+ package com.rnnetworkquality
2
+
3
+ import android.net.ConnectivityManager
4
+ import android.net.LinkProperties
5
+ import android.net.Network
6
+ import android.net.NetworkCapabilities
7
+ import android.os.Build
8
+ import java.net.Inet4Address
9
+ import java.net.Inet6Address
10
+
11
+ /** Converts Android connectivity primitives into the cross-platform snapshot contract. */
12
+ internal class SnapshotBuilder(
13
+ private val connectivityManager: ConnectivityManager,
14
+ private val cellularInfo: CellularInfo,
15
+ ) {
16
+ fun build(
17
+ network: Network?,
18
+ capabilities: NetworkCapabilities?,
19
+ linkProperties: LinkProperties?,
20
+ timestamp: Long = System.currentTimeMillis(),
21
+ ): NetworkSnapshot {
22
+ if (network == null || capabilities == null) {
23
+ return NetworkSnapshot.disconnected(timestamp, isConstrained())
24
+ }
25
+
26
+ val hasWifi = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
27
+ val hasCellular = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
28
+ val hasEthernet = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)
29
+ val hasUsb =
30
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
31
+ capabilities.hasTransport(NetworkCapabilities.TRANSPORT_USB)
32
+ val hasBluetooth = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH)
33
+ val hasVpn = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)
34
+ val transportMapping = Mappers.mapTransport(
35
+ hasNetwork = true,
36
+ hasWifi = hasWifi,
37
+ hasCellular = hasCellular,
38
+ hasEthernet = hasEthernet,
39
+ hasUsb = hasUsb,
40
+ hasBluetooth = hasBluetooth,
41
+ hasVpn = hasVpn,
42
+ )
43
+
44
+ val temporarilyNotMetered =
45
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
46
+ capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED)
47
+ val isExpensive =
48
+ !capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) &&
49
+ !temporarilyNotMetered
50
+
51
+ val downlink = capabilities.linkDownstreamBandwidthKbps.takeIf { it > 0 }
52
+ val uplink = capabilities.linkUpstreamBandwidthKbps.takeIf { it > 0 }
53
+ val signal = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
54
+ capabilities.signalStrength.takeUnless {
55
+ it == NetworkCapabilities.SIGNAL_STRENGTH_UNSPECIFIED
56
+ }
57
+ } else {
58
+ null
59
+ }
60
+
61
+ return NetworkSnapshot(
62
+ isConnected = capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET),
63
+ isValidated = capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED),
64
+ isCaptivePortal = capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL),
65
+ transport = transportMapping.transport,
66
+ isVpn = transportMapping.isVpn,
67
+ isExpensive = isExpensive,
68
+ isConstrained = isConstrained(),
69
+ isRoaming = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
70
+ !capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)
71
+ } else {
72
+ null
73
+ },
74
+ downlinkKbps = downlink,
75
+ uplinkKbps = uplink,
76
+ signalStrength = signal,
77
+ cellularGeneration = cellularInfo.generation(transportMapping.transport == "cellular"),
78
+ supportsIPv4 = linkProperties?.linkAddresses?.any { it.address is Inet4Address },
79
+ supportsIPv6 = linkProperties?.linkAddresses?.any { it.address is Inet6Address },
80
+ supportsDNS = linkProperties?.dnsServers?.isNotEmpty(),
81
+ unsatisfiedReason = null,
82
+ timestamp = timestamp,
83
+ )
84
+ }
85
+
86
+ fun isConstrained(): Boolean = try {
87
+ connectivityManager.restrictBackgroundStatus ==
88
+ ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED
89
+ } catch (_: RuntimeException) {
90
+ false
91
+ }
92
+ }
@@ -0,0 +1,100 @@
1
+ package com.rnnetworkquality
2
+
3
+ internal fun interface Cancellable {
4
+ fun cancel()
5
+ }
6
+
7
+ internal fun interface Clock {
8
+ fun nowMs(): Long
9
+ }
10
+
11
+ internal fun interface Scheduler {
12
+ fun schedule(delayMs: Long, task: () -> Unit): Cancellable
13
+ }
14
+
15
+ /**
16
+ * De-duplicates snapshots and applies leading/trailing throttling to minor signal changes.
17
+ * All calls must be serialized by the owner; the class itself has no platform dependencies.
18
+ */
19
+ internal class Throttler(
20
+ private val clock: Clock,
21
+ private val scheduler: Scheduler,
22
+ private val emit: (NetworkSnapshot) -> Unit,
23
+ throttleMs: Long,
24
+ bandwidthChangeThresholdPct: Double,
25
+ ) {
26
+ private var throttleMs = throttleMs.coerceAtLeast(0L)
27
+ private var bandwidthChangeThresholdPct = bandwidthChangeThresholdPct.coerceAtLeast(0.0)
28
+ private var lastEmitted: NetworkSnapshot? = null
29
+ private var lastEmitTime = 0L
30
+ private var pending: NetworkSnapshot? = null
31
+ private var timer: Cancellable? = null
32
+
33
+ fun updateOptions(throttleMs: Long, bandwidthChangeThresholdPct: Double) {
34
+ this.throttleMs = throttleMs.coerceAtLeast(0L)
35
+ this.bandwidthChangeThresholdPct = bandwidthChangeThresholdPct.coerceAtLeast(0.0)
36
+
37
+ val latestPending = pending ?: return
38
+ cancelPending()
39
+ submit(latestPending)
40
+ }
41
+
42
+ fun submit(snapshot: NetworkSnapshot) {
43
+ val previous = lastEmitted
44
+ if (previous == null) {
45
+ emitNow(snapshot)
46
+ return
47
+ }
48
+
49
+ if (snapshot.equalsIgnoringTimestamp(previous)) {
50
+ cancelPending()
51
+ return
52
+ }
53
+
54
+ if (Mappers.isSignificantChange(previous, snapshot)) {
55
+ cancelPending()
56
+ emitNow(snapshot)
57
+ return
58
+ }
59
+
60
+ if (!Mappers.exceedsAnyMinorThreshold(previous, snapshot, bandwidthChangeThresholdPct)) {
61
+ cancelPending()
62
+ return
63
+ }
64
+
65
+ val elapsed = (clock.nowMs() - lastEmitTime).coerceAtLeast(0L)
66
+ if (elapsed >= throttleMs) {
67
+ cancelPending()
68
+ emitNow(snapshot)
69
+ return
70
+ }
71
+
72
+ pending = snapshot
73
+ if (timer == null) {
74
+ timer = scheduler.schedule(throttleMs - elapsed) {
75
+ timer = null
76
+ val trailing = pending
77
+ pending = null
78
+ if (trailing != null) emitNow(trailing)
79
+ }
80
+ }
81
+ }
82
+
83
+ fun reset() {
84
+ cancelPending()
85
+ lastEmitted = null
86
+ lastEmitTime = 0L
87
+ }
88
+
89
+ private fun emitNow(snapshot: NetworkSnapshot) {
90
+ lastEmitted = snapshot
91
+ lastEmitTime = clock.nowMs()
92
+ emit(snapshot)
93
+ }
94
+
95
+ private fun cancelPending() {
96
+ timer?.cancel()
97
+ timer = null
98
+ pending = null
99
+ }
100
+ }
@@ -0,0 +1,47 @@
1
+ import CoreTelephony
2
+ import Foundation
3
+
4
+ final class CellularInfo {
5
+ private let networkInfo = CTTelephonyNetworkInfo()
6
+
7
+ func generation(isCellular: Bool) -> String? {
8
+ guard isCellular else { return nil }
9
+
10
+ let technology: String?
11
+ if #available(iOS 12.0, *) {
12
+ guard let serviceIdentifier = networkInfo.dataServiceIdentifier else {
13
+ return nil
14
+ }
15
+ technology = networkInfo.serviceCurrentRadioAccessTechnology?[serviceIdentifier]
16
+ } else {
17
+ technology = networkInfo.currentRadioAccessTechnology
18
+ }
19
+
20
+ guard let technology else { return nil }
21
+ return Self.map(technology)
22
+ }
23
+
24
+ private static func map(_ technology: String) -> String? {
25
+ switch technology {
26
+ case CTRadioAccessTechnologyGPRS,
27
+ CTRadioAccessTechnologyEdge,
28
+ CTRadioAccessTechnologyCDMA1x:
29
+ return "2g"
30
+ case CTRadioAccessTechnologyWCDMA,
31
+ CTRadioAccessTechnologyHSDPA,
32
+ CTRadioAccessTechnologyHSUPA,
33
+ CTRadioAccessTechnologyCDMAEVDORev0,
34
+ CTRadioAccessTechnologyCDMAEVDORevA,
35
+ CTRadioAccessTechnologyCDMAEVDORevB,
36
+ CTRadioAccessTechnologyeHRPD:
37
+ return "3g"
38
+ case CTRadioAccessTechnologyLTE:
39
+ return "4g"
40
+ case CTRadioAccessTechnologyNR,
41
+ CTRadioAccessTechnologyNRNSA:
42
+ return "5g"
43
+ default:
44
+ return nil
45
+ }
46
+ }
47
+ }