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,60 @@
1
+ buildscript {
2
+ ext.RNNetworkQuality = [
3
+ kotlinVersion: "2.2.0",
4
+ minSdkVersion: 24,
5
+ compileSdkVersion: 37
6
+ ]
7
+
8
+ ext.getExtOrDefault = { prop ->
9
+ if (rootProject.ext.has(prop)) {
10
+ return rootProject.ext.get(prop)
11
+ }
12
+
13
+ return RNNetworkQuality[prop]
14
+ }
15
+
16
+ repositories {
17
+ google()
18
+ mavenCentral()
19
+ }
20
+
21
+ dependencies {
22
+ classpath "com.android.tools.build:gradle:9.2.1"
23
+ // noinspection DifferentKotlinGradleVersion
24
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
25
+ }
26
+ }
27
+
28
+
29
+ apply plugin: "com.android.library"
30
+
31
+ if (project.extensions.findByName("kotlin") == null) {
32
+ apply plugin: "kotlin-android"
33
+ }
34
+
35
+
36
+ apply plugin: "com.facebook.react"
37
+
38
+ android {
39
+ namespace "com.rnnetworkquality"
40
+
41
+ compileSdkVersion getExtOrDefault("compileSdkVersion")
42
+
43
+ defaultConfig {
44
+ minSdkVersion getExtOrDefault("minSdkVersion")
45
+ }
46
+
47
+ compileOptions {
48
+ sourceCompatibility JavaVersion.VERSION_17
49
+ targetCompatibility JavaVersion.VERSION_17
50
+ }
51
+
52
+ testOptions {
53
+ unitTests.returnDefaultValues = true
54
+ }
55
+ }
56
+
57
+ dependencies {
58
+ implementation "com.facebook.react:react-android"
59
+ testImplementation "junit:junit:4.13.2"
60
+ }
@@ -0,0 +1,3 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
3
+ </manifest>
@@ -0,0 +1,34 @@
1
+ package com.rnnetworkquality
2
+
3
+ import android.Manifest
4
+ import android.annotation.SuppressLint
5
+ import android.content.Context
6
+ import android.content.pm.PackageManager
7
+ import android.telephony.TelephonyManager
8
+ import androidx.core.content.ContextCompat
9
+
10
+ /** Reads optional cellular generation only when the host app granted phone-state access. */
11
+ internal class CellularInfo(context: Context) {
12
+ private val appContext = context.applicationContext
13
+ private val telephonyManager =
14
+ appContext.getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager
15
+
16
+ @SuppressLint("MissingPermission")
17
+ fun generation(isCellular: Boolean): String? {
18
+ if (!isCellular || telephonyManager == null) return null
19
+ if (
20
+ ContextCompat.checkSelfPermission(appContext, Manifest.permission.READ_PHONE_STATE) !=
21
+ PackageManager.PERMISSION_GRANTED
22
+ ) {
23
+ return null
24
+ }
25
+
26
+ return try {
27
+ Mappers.mapCellularGeneration(telephonyManager.dataNetworkType)
28
+ } catch (_: SecurityException) {
29
+ null
30
+ } catch (_: RuntimeException) {
31
+ null
32
+ }
33
+ }
34
+ }
@@ -0,0 +1,26 @@
1
+ package com.rnnetworkquality
2
+
3
+ internal object DownloadPolicy {
4
+ const val MINIMUM_BYTES = 64 * 1_024L
5
+ const val MINIMUM_DURATION_MS = 100.0
6
+
7
+ fun shouldStop(
8
+ bytesReceived: Long,
9
+ elapsedMs: Double,
10
+ maximumBytes: Long,
11
+ maximumDurationMs: Long,
12
+ ): Boolean = bytesReceived >= maximumBytes || elapsedMs >= maximumDurationMs
13
+
14
+ fun measurementError(bytesReceived: Long, elapsedMs: Double): String? = when {
15
+ bytesReceived < MINIMUM_BYTES -> "too-little-data"
16
+ elapsedMs < MINIMUM_DURATION_MS -> "too-fast-to-measure"
17
+ else -> null
18
+ }
19
+
20
+ fun throughputKbps(bytesReceived: Long, elapsedMs: Double): Double? =
21
+ if (measurementError(bytesReceived, elapsedMs) == null) {
22
+ bytesReceived * 8.0 / elapsedMs
23
+ } else {
24
+ null
25
+ }
26
+ }
@@ -0,0 +1,106 @@
1
+ package com.rnnetworkquality
2
+
3
+ import kotlin.math.abs
4
+
5
+ /** Pure platform-value mappings shared by the Android implementation and JVM tests. */
6
+ internal object Mappers {
7
+ data class TransportMapping(
8
+ val transport: String,
9
+ val isVpn: Boolean,
10
+ )
11
+
12
+ fun mapTransport(
13
+ hasNetwork: Boolean,
14
+ hasWifi: Boolean,
15
+ hasCellular: Boolean,
16
+ hasEthernet: Boolean,
17
+ hasUsb: Boolean,
18
+ hasBluetooth: Boolean,
19
+ hasVpn: Boolean,
20
+ ): TransportMapping {
21
+ val transport = when {
22
+ !hasNetwork -> "none"
23
+ hasWifi -> "wifi"
24
+ hasCellular -> "cellular"
25
+ hasEthernet || hasUsb -> "ethernet"
26
+ hasBluetooth -> "bluetooth"
27
+ hasVpn -> "vpn"
28
+ else -> "other"
29
+ }
30
+ return TransportMapping(
31
+ transport = transport,
32
+ isVpn = hasNetwork && hasVpn,
33
+ )
34
+ }
35
+
36
+ /** Maps TelephonyManager.NETWORK_TYPE_* integer values without Android dependencies. */
37
+ fun mapCellularGeneration(networkType: Int): String? = when (networkType) {
38
+ 1, // GPRS
39
+ 2, // EDGE
40
+ 4, // CDMA
41
+ 7, // 1xRTT
42
+ 11, // IDEN
43
+ 16, // GSM
44
+ -> "2g"
45
+
46
+ 3, // UMTS
47
+ 5, // EVDO_0
48
+ 6, // EVDO_A
49
+ 8, // HSDPA
50
+ 9, // HSUPA
51
+ 10, // HSPA
52
+ 12, // EVDO_B
53
+ 14, // EHRPD
54
+ 15, // HSPAP
55
+ 17, // TD_SCDMA
56
+ -> "3g"
57
+
58
+ 13, // LTE
59
+ 18, // IWLAN
60
+ -> "4g"
61
+
62
+ 20, // NR
63
+ -> "5g"
64
+
65
+ else -> null
66
+ }
67
+
68
+ fun isSignificantChange(
69
+ previous: NetworkSnapshot?,
70
+ current: NetworkSnapshot,
71
+ ): Boolean {
72
+ if (previous == null) return true
73
+
74
+ return previous.isConnected != current.isConnected ||
75
+ previous.isValidated != current.isValidated ||
76
+ previous.isCaptivePortal != current.isCaptivePortal ||
77
+ previous.transport != current.transport ||
78
+ previous.isVpn != current.isVpn ||
79
+ previous.isExpensive != current.isExpensive ||
80
+ previous.isConstrained != current.isConstrained ||
81
+ previous.isRoaming != current.isRoaming ||
82
+ previous.cellularGeneration != current.cellularGeneration ||
83
+ previous.supportsIPv4 != current.supportsIPv4 ||
84
+ previous.supportsIPv6 != current.supportsIPv6 ||
85
+ previous.supportsDNS != current.supportsDNS ||
86
+ previous.unsatisfiedReason != current.unsatisfiedReason
87
+ }
88
+
89
+ fun exceedsThreshold(oldValue: Int?, newValue: Int?, thresholdPct: Double): Boolean {
90
+ if (oldValue == newValue) return false
91
+ if (oldValue == null || newValue == null) return true
92
+ if (oldValue == 0) return newValue != 0
93
+
94
+ val percentChange = abs(newValue.toDouble() - oldValue) / abs(oldValue.toDouble()) * 100.0
95
+ return percentChange >= thresholdPct.coerceAtLeast(0.0)
96
+ }
97
+
98
+ fun exceedsAnyMinorThreshold(
99
+ previous: NetworkSnapshot,
100
+ current: NetworkSnapshot,
101
+ thresholdPct: Double,
102
+ ): Boolean =
103
+ exceedsThreshold(previous.downlinkKbps, current.downlinkKbps, thresholdPct) ||
104
+ exceedsThreshold(previous.uplinkKbps, current.uplinkKbps, thresholdPct) ||
105
+ exceedsThreshold(previous.signalStrength, current.signalStrength, thresholdPct)
106
+ }
@@ -0,0 +1,263 @@
1
+ package com.rnnetworkquality
2
+
3
+ import android.content.BroadcastReceiver
4
+ import android.content.Context
5
+ import android.content.Intent
6
+ import android.content.IntentFilter
7
+ import android.net.ConnectivityManager
8
+ import android.net.LinkProperties
9
+ import android.net.Network
10
+ import android.net.NetworkCapabilities
11
+ import android.os.Build
12
+ import android.os.Handler
13
+ import android.os.Looper
14
+ import android.util.Log
15
+ import androidx.core.content.ContextCompat
16
+
17
+ /** Owns the event-driven default-network callback and Data Saver receiver. */
18
+ internal class NetworkMonitor(
19
+ context: Context,
20
+ private val connectivityManager: ConnectivityManager,
21
+ private val handler: Handler,
22
+ private val snapshotBuilder: SnapshotBuilder,
23
+ onSnapshot: (NetworkSnapshot) -> Unit,
24
+ ) {
25
+ private val appContext = context.applicationContext
26
+ private val throttler = Throttler(
27
+ clock = Clock { android.os.SystemClock.elapsedRealtime() },
28
+ scheduler = HandlerScheduler(handler),
29
+ emit = onSnapshot,
30
+ throttleMs = DEFAULT_THROTTLE_MS,
31
+ bandwidthChangeThresholdPct = DEFAULT_THRESHOLD_PERCENT,
32
+ )
33
+
34
+ private var running = false
35
+ private var callbackRegistered = false
36
+ private var receiverRegistered = false
37
+ private var latestNetwork: Network? = null
38
+ private var latestCapabilities: NetworkCapabilities? = null
39
+ private var latestLinkProperties: LinkProperties? = null
40
+
41
+ private val networkCallback = object : ConnectivityManager.NetworkCallback() {
42
+ override fun onAvailable(network: Network) = dispatch {
43
+ if (!running || currentActiveNetwork() != network) return@dispatch
44
+ latestNetwork = network
45
+ latestCapabilities = safeCapabilities(network)
46
+ latestLinkProperties = safeLinkProperties(network)
47
+ rebuildSnapshot()
48
+ }
49
+
50
+ override fun onCapabilitiesChanged(network: Network, capabilities: NetworkCapabilities) =
51
+ dispatch {
52
+ if (!running || currentActiveNetwork() != network) return@dispatch
53
+ if (latestNetwork != network) {
54
+ latestNetwork = network
55
+ latestLinkProperties = safeLinkProperties(network)
56
+ }
57
+ latestCapabilities = capabilities
58
+ rebuildSnapshot()
59
+ }
60
+
61
+ override fun onLinkPropertiesChanged(network: Network, linkProperties: LinkProperties) =
62
+ dispatch {
63
+ if (!running || currentActiveNetwork() != network) return@dispatch
64
+ if (latestNetwork != network) {
65
+ latestNetwork = network
66
+ latestCapabilities = safeCapabilities(network)
67
+ }
68
+ latestLinkProperties = linkProperties
69
+ rebuildSnapshot()
70
+ }
71
+
72
+ override fun onLost(network: Network) = dispatch {
73
+ if (!running || (latestNetwork != null && latestNetwork != network)) return@dispatch
74
+ val activeNetwork = currentActiveNetwork()
75
+ if (activeNetwork != null && activeNetwork != network) {
76
+ refreshFromSystem()
77
+ return@dispatch
78
+ }
79
+ latestNetwork = null
80
+ latestCapabilities = null
81
+ latestLinkProperties = null
82
+ throttler.submit(
83
+ NetworkSnapshot.disconnected(
84
+ timestamp = System.currentTimeMillis(),
85
+ isConstrained = snapshotBuilder.isConstrained(),
86
+ )
87
+ )
88
+ }
89
+
90
+ override fun onBlockedStatusChanged(network: Network, blocked: Boolean) = dispatch {
91
+ if (!running || currentActiveNetwork() != network) return@dispatch
92
+ refreshFromSystem()
93
+ }
94
+ }
95
+
96
+ private val dataSaverReceiver = object : BroadcastReceiver() {
97
+ override fun onReceive(context: Context?, intent: Intent?) {
98
+ if (intent?.action == ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED) {
99
+ dispatch {
100
+ if (running) refreshFromSystem()
101
+ }
102
+ }
103
+ }
104
+ }
105
+
106
+ fun start(throttleMs: Long, bandwidthChangeThresholdPct: Double): Boolean {
107
+ checkHandlerThread()
108
+ throttler.updateOptions(throttleMs, bandwidthChangeThresholdPct)
109
+ if (running) return true
110
+
111
+ throttler.reset()
112
+ running = true
113
+ if (!registerNetworkCallback()) {
114
+ refreshFromSystem()
115
+ running = false
116
+ return false
117
+ }
118
+ if (!registerDataSaverReceiver()) {
119
+ unregisterNetworkCallback()
120
+ refreshFromSystem()
121
+ running = false
122
+ return false
123
+ }
124
+ refreshFromSystem()
125
+ return true
126
+ }
127
+
128
+ fun stop() {
129
+ checkHandlerThread()
130
+ running = false
131
+ unregisterNetworkCallback()
132
+ unregisterDataSaverReceiver()
133
+ latestNetwork = null
134
+ latestCapabilities = null
135
+ latestLinkProperties = null
136
+ throttler.reset()
137
+ }
138
+
139
+ /** Must be called on the monitor handler. Reads current state even when monitoring is stopped. */
140
+ fun readCurrentSnapshot(): NetworkSnapshot {
141
+ checkHandlerThread()
142
+ val network = currentActiveNetwork()
143
+ return snapshotBuilder.build(
144
+ network = network,
145
+ capabilities = network?.let(::safeCapabilities),
146
+ linkProperties = network?.let(::safeLinkProperties),
147
+ )
148
+ }
149
+
150
+ private fun registerNetworkCallback(): Boolean = try {
151
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
152
+ connectivityManager.registerDefaultNetworkCallback(networkCallback, handler)
153
+ } else {
154
+ connectivityManager.registerDefaultNetworkCallback(networkCallback)
155
+ }
156
+ callbackRegistered = true
157
+ true
158
+ } catch (error: RuntimeException) {
159
+ callbackRegistered = false
160
+ Log.w(TAG, "Unable to register default-network callback", error)
161
+ false
162
+ }
163
+
164
+ private fun registerDataSaverReceiver(): Boolean = try {
165
+ ContextCompat.registerReceiver(
166
+ appContext,
167
+ dataSaverReceiver,
168
+ IntentFilter(ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED),
169
+ ContextCompat.RECEIVER_NOT_EXPORTED,
170
+ )
171
+ receiverRegistered = true
172
+ true
173
+ } catch (error: RuntimeException) {
174
+ receiverRegistered = false
175
+ Log.w(TAG, "Unable to register Data Saver receiver", error)
176
+ false
177
+ }
178
+
179
+ private fun unregisterNetworkCallback() {
180
+ if (!callbackRegistered) return
181
+ try {
182
+ connectivityManager.unregisterNetworkCallback(networkCallback)
183
+ } catch (error: RuntimeException) {
184
+ Log.w(TAG, "Unable to unregister default-network callback", error)
185
+ } finally {
186
+ callbackRegistered = false
187
+ }
188
+ }
189
+
190
+ private fun unregisterDataSaverReceiver() {
191
+ if (!receiverRegistered) return
192
+ try {
193
+ appContext.unregisterReceiver(dataSaverReceiver)
194
+ } catch (error: RuntimeException) {
195
+ Log.w(TAG, "Unable to unregister Data Saver receiver", error)
196
+ } finally {
197
+ receiverRegistered = false
198
+ }
199
+ }
200
+
201
+ private fun refreshFromSystem() {
202
+ val network = currentActiveNetwork()
203
+ latestNetwork = network
204
+ latestCapabilities = network?.let(::safeCapabilities)
205
+ latestLinkProperties = network?.let(::safeLinkProperties)
206
+ rebuildSnapshot()
207
+ }
208
+
209
+ private fun rebuildSnapshot() {
210
+ throttler.submit(
211
+ snapshotBuilder.build(
212
+ network = latestNetwork,
213
+ capabilities = latestCapabilities,
214
+ linkProperties = latestLinkProperties,
215
+ )
216
+ )
217
+ }
218
+
219
+ private fun safeCapabilities(network: Network): NetworkCapabilities? = try {
220
+ connectivityManager.getNetworkCapabilities(network)
221
+ } catch (error: RuntimeException) {
222
+ Log.w(TAG, "Unable to read network capabilities", error)
223
+ null
224
+ }
225
+
226
+ private fun safeLinkProperties(network: Network): LinkProperties? = try {
227
+ connectivityManager.getLinkProperties(network)
228
+ } catch (error: RuntimeException) {
229
+ Log.w(TAG, "Unable to read link properties", error)
230
+ null
231
+ }
232
+
233
+ private fun currentActiveNetwork(): Network? = try {
234
+ connectivityManager.activeNetwork
235
+ } catch (error: RuntimeException) {
236
+ Log.w(TAG, "Unable to read the active network", error)
237
+ null
238
+ }
239
+
240
+ private fun dispatch(block: () -> Unit) {
241
+ if (Looper.myLooper() == handler.looper) block() else handler.post(block)
242
+ }
243
+
244
+ private fun checkHandlerThread() {
245
+ check(Looper.myLooper() == handler.looper) {
246
+ "NetworkMonitor must only be accessed from its handler thread"
247
+ }
248
+ }
249
+
250
+ private class HandlerScheduler(private val handler: Handler) : Scheduler {
251
+ override fun schedule(delayMs: Long, task: () -> Unit): Cancellable {
252
+ val runnable = Runnable(task)
253
+ handler.postDelayed(runnable, delayMs.coerceAtLeast(0L))
254
+ return Cancellable { handler.removeCallbacks(runnable) }
255
+ }
256
+ }
257
+
258
+ companion object {
259
+ private const val TAG = "RNNetworkQuality"
260
+ private const val DEFAULT_THROTTLE_MS = 1_000L
261
+ private const val DEFAULT_THRESHOLD_PERCENT = 10.0
262
+ }
263
+ }