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.
- package/LICENSE +20 -0
- package/README.md +899 -0
- package/android/build.gradle +60 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/com/rnnetworkquality/CellularInfo.kt +34 -0
- package/android/src/main/java/com/rnnetworkquality/DownloadPolicy.kt +26 -0
- package/android/src/main/java/com/rnnetworkquality/Mappers.kt +106 -0
- package/android/src/main/java/com/rnnetworkquality/NetworkMonitor.kt +263 -0
- package/android/src/main/java/com/rnnetworkquality/NetworkProbe.kt +668 -0
- package/android/src/main/java/com/rnnetworkquality/NetworkQualityModule.kt +287 -0
- package/android/src/main/java/com/rnnetworkquality/NetworkQualityPackage.kt +25 -0
- package/android/src/main/java/com/rnnetworkquality/NetworkSnapshot.kt +83 -0
- package/android/src/main/java/com/rnnetworkquality/SnapshotBuilder.kt +92 -0
- package/android/src/main/java/com/rnnetworkquality/Throttler.kt +100 -0
- package/ios/CellularInfo.swift +47 -0
- package/ios/NetworkProbe.swift +391 -0
- package/ios/NetworkQuality.h +8 -0
- package/ios/NetworkQuality.mm +88 -0
- package/ios/NetworkQualityImpl.swift +371 -0
- package/ios/PathSnapshot.swift +109 -0
- package/ios/PrivacyInfo.xcprivacy +23 -0
- package/ios/Throttler.swift +174 -0
- package/lib/module/NativeNetworkQuality.js +5 -0
- package/lib/module/NativeNetworkQuality.js.map +1 -0
- package/lib/module/classify.js +126 -0
- package/lib/module/classify.js.map +1 -0
- package/lib/module/constants.js +52 -0
- package/lib/module/constants.js.map +1 -0
- package/lib/module/errors.js +18 -0
- package/lib/module/errors.js.map +1 -0
- package/lib/module/hooks.js +78 -0
- package/lib/module/hooks.js.map +1 -0
- package/lib/module/index.js +19 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/manager.js +595 -0
- package/lib/module/manager.js.map +1 -0
- package/lib/module/normalize.js +35 -0
- package/lib/module/normalize.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeNetworkQuality.d.ts +50 -0
- package/lib/typescript/src/NativeNetworkQuality.d.ts.map +1 -0
- package/lib/typescript/src/classify.d.ts +12 -0
- package/lib/typescript/src/classify.d.ts.map +1 -0
- package/lib/typescript/src/constants.d.ts +15 -0
- package/lib/typescript/src/constants.d.ts.map +1 -0
- package/lib/typescript/src/errors.d.ts +13 -0
- package/lib/typescript/src/errors.d.ts.map +1 -0
- package/lib/typescript/src/hooks.d.ts +18 -0
- package/lib/typescript/src/hooks.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +13 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/manager.d.ts +98 -0
- package/lib/typescript/src/manager.d.ts.map +1 -0
- package/lib/typescript/src/normalize.d.ts +5 -0
- package/lib/typescript/src/normalize.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +171 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/mock.js +318 -0
- package/package.json +161 -0
- package/rn-network-quality.podspec +32 -0
- package/src/NativeNetworkQuality.ts +58 -0
- package/src/classify.ts +208 -0
- package/src/constants.ts +48 -0
- package/src/errors.ts +23 -0
- package/src/hooks.ts +110 -0
- package/src/index.tsx +25 -0
- package/src/manager.ts +891 -0
- package/src/normalize.ts +81 -0
- 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,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
|
+
}
|