@mentra/acs-meeting 3.2.0-dev.223 → 3.2.0-dev.225
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/README.md +14 -3
- package/android/build.gradle +1 -0
- package/android/src/main/java/com/mentra/acsmeeting/AcsMeetingModule.kt +10 -10
- package/android/src/main/java/com/mentra/acsmeeting/AcsMeetingSession.kt +37 -37
- package/android/src/main/java/com/mentra/acsmeeting/audio/UplinkSender.kt +4 -4
- package/android/src/main/java/com/mentra/acsmeeting/source/MeetingVideoSourceSpec.kt +3 -0
- package/android/src/main/java/com/mentra/acsmeeting/video/AcsFrameSender.kt +12 -10
- package/android/src/main/java/com/mentra/acsmeeting/video/I420FormatSpec.kt +2 -0
- package/android/src/main/java/com/mentra/acsmeeting/video/Nv12Packer.kt +2 -0
- package/android/src/test/java/com/mentra/acsmeeting/source/{AcsInvestigationTest.kt → MediaDiagnosticsTest.kt} +10 -10
- package/android/src/test/java/com/mentra/acsmeeting/source/MeetingVideoSourceSpecTest.kt +3 -0
- package/android/src/test/java/com/mentra/acsmeeting/telemetry/VideoQualityTest.kt +2 -2
- package/android/src/test/java/com/mentra/acsmeeting/video/I420FormatSpecTest.kt +2 -0
- package/android/src/test/java/com/mentra/acsmeeting/video/VideoProfileTest.kt +2 -2
- package/build/AcsMeeting.types.d.ts +10 -1
- package/build/AcsMeeting.types.d.ts.map +1 -1
- package/build/AcsMeeting.types.js.map +1 -1
- package/build/AcsMeetingModule.d.ts +2 -2
- package/build/AcsMeetingModule.d.ts.map +1 -1
- package/build/AcsMeetingModule.js.map +1 -1
- package/ios/AcsMeeting.podspec +1 -1
- package/ios/AcsMeetingModule.swift +284 -148
- package/ios/PolicyKit/Package.swift +2 -1
- package/ios/PolicyKit/Sources/AcsAudioPolicy/CallbackOperation.swift +60 -0
- package/ios/PolicyKit/Sources/AcsAudioPolicy/CapturePolicy.swift +5 -0
- package/ios/PolicyKit/Tests/AcsAudioPolicyTests/AcsAudioPolicyTests.swift +1 -0
- package/ios/PolicyKit/Tests/AcsAudioPolicyTests/CallAgentRetirementTests.swift +113 -0
- package/package.json +3 -2
- package/src/AcsMeeting.types.ts +4 -1
- package/src/AcsMeetingModule.ts +2 -2
- package/android/src/androidTest/java/com/mentra/acsmeeting/network/SoftApFeasibilityGateTest.kt +0 -291
- package/android/src/main/java/com/mentra/acsmeeting/network/InternetHold.kt +0 -501
- package/android/src/main/java/com/mentra/acsmeeting/network/Ipv4Prefix.kt +0 -51
- package/android/src/main/java/com/mentra/acsmeeting/network/NetworkFacts.kt +0 -81
- package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkChangeDetector.kt +0 -355
- package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkError.kt +0 -109
- package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkObserver.kt +0 -160
- package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkReadiness.kt +0 -103
- package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkRequestSpec.kt +0 -53
- package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkState.kt +0 -127
- package/android/src/main/java/com/mentra/acsmeeting/network/ScopedSoftApNetwork.kt +0 -573
- package/android/src/main/java/com/mentra/acsmeeting/source/BitmapFont.kt +0 -99
- package/android/src/main/java/com/mentra/acsmeeting/source/CloudflareWhepSource.kt +0 -487
- package/android/src/main/java/com/mentra/acsmeeting/source/DecodedTrackRelay.kt +0 -147
- package/android/src/main/java/com/mentra/acsmeeting/source/FirstFrameGate.kt +0 -45
- package/android/src/main/java/com/mentra/acsmeeting/source/GlassesMediaSource.kt +0 -143
- package/android/src/main/java/com/mentra/acsmeeting/source/LocalWhipIngestSource.kt +0 -772
- package/android/src/main/java/com/mentra/acsmeeting/source/MediaListeners.kt +0 -57
- package/android/src/main/java/com/mentra/acsmeeting/source/SdpAdapter.kt +0 -17
- package/android/src/main/java/com/mentra/acsmeeting/source/SelectedIcePair.kt +0 -139
- package/android/src/main/java/com/mentra/acsmeeting/source/SoftApIceDiagnosis.kt +0 -144
- package/android/src/main/java/com/mentra/acsmeeting/source/SoftApIcePolicy.kt +0 -46
- package/android/src/main/java/com/mentra/acsmeeting/source/SoftApSdpGuard.kt +0 -160
- package/android/src/main/java/com/mentra/acsmeeting/source/SyntheticFrameFactory.kt +0 -205
- package/android/src/main/java/com/mentra/acsmeeting/source/SyntheticI420Source.kt +0 -214
- package/android/src/main/java/com/mentra/acsmeeting/source/TrackRegistry.kt +0 -33
- package/android/src/main/java/com/mentra/acsmeeting/source/VideoSourceArm.kt +0 -110
- package/android/src/main/java/com/mentra/acsmeeting/source/WhipIngestProtocol.kt +0 -207
- package/android/src/main/java/com/mentra/acsmeeting/source/WhipIngestServer.kt +0 -356
- package/android/src/main/java/com/mentra/acsmeeting/telemetry/AvSyncProbe.kt +0 -155
- package/android/src/main/java/com/mentra/acsmeeting/telemetry/ChromaProbe.kt +0 -78
- package/android/src/main/java/com/mentra/acsmeeting/telemetry/PipelineStats.kt +0 -372
- package/android/src/main/java/com/mentra/acsmeeting/telemetry/PipelineTicker.kt +0 -79
- package/android/src/main/java/com/mentra/acsmeeting/telemetry/ProcessCpu.kt +0 -15
- package/android/src/main/java/com/mentra/acsmeeting/telemetry/RingPercentile.kt +0 -48
- package/android/src/main/java/com/mentra/acsmeeting/telemetry/VideoQuality.kt +0 -178
- package/android/src/main/java/com/mentra/acsmeeting/telemetry/VideoRateVerdict.kt +0 -145
- package/android/src/main/java/com/mentra/acsmeeting/trace/SoftApTrace.kt +0 -143
- package/android/src/main/java/com/mentra/acsmeeting/video/FrameGeometry.kt +0 -22
- package/android/src/main/java/com/mentra/acsmeeting/video/I420Packer.kt +0 -72
- package/android/src/test/java/com/mentra/acsmeeting/network/InternetHoldLifecycleTest.kt +0 -102
- package/android/src/test/java/com/mentra/acsmeeting/network/InternetHoldTest.kt +0 -148
- package/android/src/test/java/com/mentra/acsmeeting/network/Ipv4PrefixTest.kt +0 -59
- package/android/src/test/java/com/mentra/acsmeeting/network/NetworkFactsTest.kt +0 -59
- package/android/src/test/java/com/mentra/acsmeeting/network/ScopedLocalNetworkPermissionTest.kt +0 -35
- package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkChangeDetectorTest.kt +0 -165
- package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkErrorTest.kt +0 -96
- package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkLifecycleTest.kt +0 -187
- package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkObserverTest.kt +0 -172
- package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkReadinessTest.kt +0 -141
- package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkRequestSpecTest.kt +0 -69
- package/android/src/test/java/com/mentra/acsmeeting/source/BitmapFontTest.kt +0 -22
- package/android/src/test/java/com/mentra/acsmeeting/source/DecodedTrackRelayTest.kt +0 -41
- package/android/src/test/java/com/mentra/acsmeeting/source/FirstFrameGateTest.kt +0 -60
- package/android/src/test/java/com/mentra/acsmeeting/source/GlassesMediaSourceTest.kt +0 -151
- package/android/src/test/java/com/mentra/acsmeeting/source/I420PlanesTest.kt +0 -67
- package/android/src/test/java/com/mentra/acsmeeting/source/SelectedIcePairTest.kt +0 -165
- package/android/src/test/java/com/mentra/acsmeeting/source/SoftApIceDiagnosisTest.kt +0 -148
- package/android/src/test/java/com/mentra/acsmeeting/source/SoftApIcePolicyTest.kt +0 -76
- package/android/src/test/java/com/mentra/acsmeeting/source/SoftApSdpGuardTest.kt +0 -275
- package/android/src/test/java/com/mentra/acsmeeting/source/SyntheticFrameFactoryTest.kt +0 -193
- package/android/src/test/java/com/mentra/acsmeeting/source/SyntheticI420SourceTest.kt +0 -176
- package/android/src/test/java/com/mentra/acsmeeting/source/TrackRegistryTest.kt +0 -45
- package/android/src/test/java/com/mentra/acsmeeting/source/WhipIngestProtocolTest.kt +0 -356
- package/android/src/test/java/com/mentra/acsmeeting/source/WhipIngestServerLoopbackTest.kt +0 -411
- package/android/src/test/java/com/mentra/acsmeeting/telemetry/AvSyncProbeTest.kt +0 -83
- package/android/src/test/java/com/mentra/acsmeeting/telemetry/ChromaProbeTest.kt +0 -61
- package/android/src/test/java/com/mentra/acsmeeting/telemetry/PipelineStatsTest.kt +0 -322
- package/android/src/test/java/com/mentra/acsmeeting/telemetry/ProcessCpuTest.kt +0 -18
- package/android/src/test/java/com/mentra/acsmeeting/telemetry/RingPercentileTest.kt +0 -49
- package/android/src/test/java/com/mentra/acsmeeting/telemetry/VideoRateVerdictTest.kt +0 -149
- package/android/src/test/java/com/mentra/acsmeeting/trace/SoftApTraceTest.kt +0 -97
- package/android/src/test/java/com/mentra/acsmeeting/video/FrameGeometryTest.kt +0 -33
- package/android/src/test/java/com/mentra/acsmeeting/video/I420PackerTest.kt +0 -126
- package/ios/PolicyKit/Sources/AcsAudioPolicy/GlassesMediaSource.swift +0 -68
- package/ios/WhepVideoSource.swift +0 -356
|
@@ -1,501 +0,0 @@
|
|
|
1
|
-
package com.mentra.acsmeeting.network
|
|
2
|
-
|
|
3
|
-
import android.content.Context
|
|
4
|
-
import android.net.ConnectivityManager
|
|
5
|
-
import android.net.LinkProperties
|
|
6
|
-
import android.net.Network
|
|
7
|
-
import android.net.NetworkCapabilities
|
|
8
|
-
import android.net.NetworkRequest
|
|
9
|
-
import android.net.wifi.WifiManager
|
|
10
|
-
import android.os.SystemClock
|
|
11
|
-
import com.mentra.acsmeeting.trace.SoftApTrace
|
|
12
|
-
import java.net.Inet4Address
|
|
13
|
-
import java.util.concurrent.CountDownLatch
|
|
14
|
-
import java.util.concurrent.TimeUnit
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Keeps the phone's internet on cellular across the hotspot join, and says so honestly.
|
|
18
|
-
*
|
|
19
|
-
* Joining the glasses' SoftAP takes the phone off office Wi-Fi. Cellular becomes the default route,
|
|
20
|
-
* and on device it was not yet validated when that happened: `scoped_network_available` at 18:04:57
|
|
21
|
-
* was followed by a 30 s gap before ACS even began its native join, then a 40 s step timeout, with
|
|
22
|
-
* device-wide DNS and TLS deadline failures throughout. ACS needs the internet during that window,
|
|
23
|
-
* so the transition has to be deliberate rather than hoped for.
|
|
24
|
-
*
|
|
25
|
-
* ## What a request does and does not buy
|
|
26
|
-
*
|
|
27
|
-
* `requestNetwork(CELLULAR + INTERNET)` asks Android to bring cellular up and keeps it up while the
|
|
28
|
-
* request is held. It does **not** guarantee the network is validated, and it does not switch the
|
|
29
|
-
* default route on any particular schedule. So this class does two separate things, and neither is
|
|
30
|
-
* inferred from the other:
|
|
31
|
-
*
|
|
32
|
-
* - [awaitValidatedCellular] holds the request and waits for `NET_CAPABILITY_VALIDATED` on the
|
|
33
|
-
* cellular network itself. Run *before* leaving Wi-Fi, so a phone with no working cellular fails
|
|
34
|
-
* with a reason instead of stranding ACS on a hotspot with no internet.
|
|
35
|
-
* - [awaitValidatedDefault] asks what the app's actual default network is now, and whether it is
|
|
36
|
-
* validated. Run *after* the hotspot join, because that is when the default can change.
|
|
37
|
-
*
|
|
38
|
-
* Holding is not the same as *using*, which is what [bindProcessToCellular] is for, and why that one
|
|
39
|
-
* has a deliberately short leash.
|
|
40
|
-
*
|
|
41
|
-
* Bounded, released on every exit, and idempotent: teardown calls [release] without checking.
|
|
42
|
-
*/
|
|
43
|
-
class InternetHold(private val context: Context) {
|
|
44
|
-
|
|
45
|
-
/** The app's default network as the framework currently reports it. */
|
|
46
|
-
data class DefaultNetwork(
|
|
47
|
-
val transport: String,
|
|
48
|
-
val validated: Boolean,
|
|
49
|
-
val present: Boolean,
|
|
50
|
-
/** The leftover glasses AP from a SoftAP call — local only, no path to Teams. */
|
|
51
|
-
val glassesHotspot: Boolean = false,
|
|
52
|
-
) {
|
|
53
|
-
/** Whether ACS (or the next `teams:create`) can be expected to reach the internet over this. */
|
|
54
|
-
val usable: Boolean
|
|
55
|
-
get() = present && validated && !glassesHotspot
|
|
56
|
-
|
|
57
|
-
override fun toString(): String {
|
|
58
|
-
if (!present) return "none"
|
|
59
|
-
val state = if (validated) "validated" else "unvalidated"
|
|
60
|
-
return if (glassesHotspot) "$transport ($state, leftover-hotspot)" else "$transport ($state)"
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Outcome of holding cellular up. [validated] is the only field worth branching on. */
|
|
65
|
-
data class CellularHold(val held: Boolean, val validated: Boolean, val waitedMs: Long)
|
|
66
|
-
|
|
67
|
-
private val lock = Any()
|
|
68
|
-
private var callback: ConnectivityManager.NetworkCallback? = null
|
|
69
|
-
private var closed = false
|
|
70
|
-
private var processPinned = false
|
|
71
|
-
private var pinnedNetwork: Network? = null
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Request cellular and wait until it validates, bounded by [timeoutMs].
|
|
75
|
-
*
|
|
76
|
-
* The request stays held afterwards — including when validation times out — so that a caller
|
|
77
|
-
* which chooses to continue anyway does not also lose the radio. [release] is the only thing
|
|
78
|
-
* that drops it.
|
|
79
|
-
*/
|
|
80
|
-
fun awaitValidatedCellular(timeoutMs: Long = CELLULAR_WAIT_MS): CellularHold {
|
|
81
|
-
val manager = connectivityManager()
|
|
82
|
-
if (manager == null) {
|
|
83
|
-
SoftApTrace.failure("cellular_hold_requested", "reason" to "no connectivity manager")
|
|
84
|
-
return CellularHold(false, false, 0)
|
|
85
|
-
}
|
|
86
|
-
val validated = CountDownLatch(1)
|
|
87
|
-
val startedAt = SystemClock.elapsedRealtime()
|
|
88
|
-
// Entry as well as outcome: this blocks for up to [timeoutMs] before anything else in the
|
|
89
|
-
// join happens, so a wearer's "nothing happened for fifteen seconds" needs a line here.
|
|
90
|
-
SoftApTrace.stage(
|
|
91
|
-
"cellular_hold_requested",
|
|
92
|
-
"timeoutMs" to timeoutMs,
|
|
93
|
-
"pinned" to synchronized(lock) { processPinned },
|
|
94
|
-
"default" to defaultNetwork().toString(),
|
|
95
|
-
)
|
|
96
|
-
|
|
97
|
-
val watcher =
|
|
98
|
-
object : ConnectivityManager.NetworkCallback() {
|
|
99
|
-
override fun onCapabilitiesChanged(
|
|
100
|
-
network: Network,
|
|
101
|
-
capabilities: NetworkCapabilities,
|
|
102
|
-
) {
|
|
103
|
-
// Validation is a capability change, not an availability one: `onAvailable`
|
|
104
|
-
// fires for a cellular network that cannot yet carry a TLS handshake.
|
|
105
|
-
if (isValidatedInternet(capabilities)) {
|
|
106
|
-
validated.countDown()
|
|
107
|
-
synchronized(lock) {
|
|
108
|
-
// Mobile data can return as a different Network. Retire the old pin
|
|
109
|
-
// without allowing a released callback to bind the next call.
|
|
110
|
-
if (callback === this && processPinned && pinnedNetwork != network) {
|
|
111
|
-
val previous = pinnedNetwork?.toString() ?: "none"
|
|
112
|
-
val ok = manager.bindProcessToNetwork(network)
|
|
113
|
-
if (ok) pinnedNetwork = network
|
|
114
|
-
SoftApTrace.stage(
|
|
115
|
-
"cellular_pin_refreshed",
|
|
116
|
-
"ok" to ok,
|
|
117
|
-
"network" to network,
|
|
118
|
-
"replaced" to previous,
|
|
119
|
-
)
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
override fun onLost(network: Network) {
|
|
126
|
-
synchronized(lock) {
|
|
127
|
-
if (callback !== this || !processPinned || pinnedNetwork != network) return
|
|
128
|
-
// Keep the intent to pin when cellular validates again, but do not leave
|
|
129
|
-
// the entire app bound to a dead network in the meantime.
|
|
130
|
-
manager.bindProcessToNetwork(null)
|
|
131
|
-
pinnedNetwork = null
|
|
132
|
-
// The intent to pin survives, so this is not a leak — but the process is
|
|
133
|
-
// unbound from here until cellular validates again, and that window is
|
|
134
|
-
// where an unbound socket can land on the glasses AP.
|
|
135
|
-
SoftApTrace.stage("cellular_pin_lost", "network" to network)
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
val request =
|
|
141
|
-
NetworkRequest.Builder()
|
|
142
|
-
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
|
|
143
|
-
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
|
144
|
-
.build()
|
|
145
|
-
|
|
146
|
-
val held =
|
|
147
|
-
runCatching {
|
|
148
|
-
synchronized(lock) {
|
|
149
|
-
if (closed) return CellularHold(false, false, 0)
|
|
150
|
-
releaseLocked(reason = "re-requesting cellular")
|
|
151
|
-
manager.requestNetwork(request, watcher)
|
|
152
|
-
callback = watcher
|
|
153
|
-
}
|
|
154
|
-
}.isSuccess
|
|
155
|
-
if (!held) {
|
|
156
|
-
SoftApTrace.failure("cellular_validated", "held" to false, "reason" to "requestNetwork threw")
|
|
157
|
-
return CellularHold(false, false, 0)
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
val ok = validated.await(timeoutMs, TimeUnit.MILLISECONDS)
|
|
161
|
-
val waitedMs = SystemClock.elapsedRealtime() - startedAt
|
|
162
|
-
// A network that was already validated before the request may never produce a callback we
|
|
163
|
-
// see, so the current state is the tiebreak rather than the latch alone.
|
|
164
|
-
val resolved = ok || cellularIsValidated(manager)
|
|
165
|
-
SoftApTrace.stage(
|
|
166
|
-
"cellular_validated",
|
|
167
|
-
"held" to true,
|
|
168
|
-
"validated" to resolved,
|
|
169
|
-
"viaCallback" to ok,
|
|
170
|
-
"waitedMs" to waitedMs,
|
|
171
|
-
)
|
|
172
|
-
// Re-pin: releaseLocked above dropped any existing pin, and the caller is about to leave
|
|
173
|
-
// Wi-Fi. The one join that ever reached CONNECTED was pinned from here onwards, i.e. across
|
|
174
|
-
// the hotspot join itself, not from just before the Teams join.
|
|
175
|
-
synchronized(lock) {
|
|
176
|
-
// Module destruction or a newer request can overtake the wait above. A retired
|
|
177
|
-
// request must never pin the process again, even if its late callback validated.
|
|
178
|
-
if (closed || callback !== watcher) return CellularHold(false, false, waitedMs)
|
|
179
|
-
if (resolved) bindProcessToCellular()
|
|
180
|
-
return CellularHold(true, resolved, waitedMs)
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Pin this process's unbound sockets onto validated cellular. Returns whether it took.
|
|
186
|
-
*
|
|
187
|
-
* Being *on* an internet-less hotspot is not the same as Android routing around it. With the
|
|
188
|
-
* scoped SoftAP joined, `getActiveNetwork` reports validated cellular and unbound sockets still
|
|
189
|
-
* fail: ACS sat in CONNECTING for 91 s and gave up with code 408, and the app's own cloud
|
|
190
|
-
* websocket dropped mid-call. The one join that reached CONNECTED — in under four seconds — was
|
|
191
|
-
* pinned continuously from before the hotspot join.
|
|
192
|
-
*
|
|
193
|
-
* Pinning does not make ACS *fast*: a cold `createCallAgent` measured 30 s pinned to cellular,
|
|
194
|
-
* about the same as unpinned. It decides whether the call connects at all, not how quickly.
|
|
195
|
-
*
|
|
196
|
-
* ## Hold it continuously; open one hole
|
|
197
|
-
*
|
|
198
|
-
* This marks *every* socket the process opens while it is in effect, and `Network.bindSocket`
|
|
199
|
-
* has no `ServerSocket` overload to undo it. A WHIP listener opened while pinned accepted on
|
|
200
|
-
* `192.168.43.79` but answered out the radio, so the glasses' SYN drew no reply at all and
|
|
201
|
-
* publish died on a 10 s connect timeout.
|
|
202
|
-
*
|
|
203
|
-
* Pinning late — after the hotspot join, just before the Teams join — does not work either:
|
|
204
|
-
* sockets keep the mark they were created with, so ACS re-binds its signalling on the broken
|
|
205
|
-
* route in the unpinned window and never recovers. So the pin is held from sign-in through
|
|
206
|
-
* teardown, and [unbindProcess] opens a hole only for the moment the WHIP `ServerSocket` is
|
|
207
|
-
* created. ICE is unaffected throughout: libwebrtc re-marks its own sockets onto the scoped
|
|
208
|
-
* network handle that `ScopedNetworkChangeDetector.toNetworkInformation` publishes.
|
|
209
|
-
*
|
|
210
|
-
* Anything that pins must unpin — via [unbindProcess], or via [release] on teardown.
|
|
211
|
-
*/
|
|
212
|
-
fun bindProcessToCellular(): Boolean = synchronized(lock) {
|
|
213
|
-
if (closed) return@synchronized false
|
|
214
|
-
val manager = connectivityManager()
|
|
215
|
-
if (manager == null) {
|
|
216
|
-
SoftApTrace.failure("process_pinned_to_cellular", "ok" to false, "reason" to "no connectivity manager")
|
|
217
|
-
return@synchronized false
|
|
218
|
-
}
|
|
219
|
-
val network = findValidatedCellular(manager)
|
|
220
|
-
if (network == null) {
|
|
221
|
-
SoftApTrace.failure("process_pinned_to_cellular", "ok" to false, "reason" to "no validated cellular")
|
|
222
|
-
return@synchronized false
|
|
223
|
-
}
|
|
224
|
-
val alreadyPinned = synchronized(lock) { pinnedNetwork }
|
|
225
|
-
val ok = runCatching { manager.bindProcessToNetwork(network) }.getOrDefault(false)
|
|
226
|
-
if (ok) synchronized(lock) {
|
|
227
|
-
processPinned = true
|
|
228
|
-
pinnedNetwork = network
|
|
229
|
-
}
|
|
230
|
-
// Which network, not just that one was taken. Mobile data comes back as a different
|
|
231
|
-
// [Network] after a hotspot join, and a pin on the previous handle is indistinguishable
|
|
232
|
-
// from a working one until ACS times out on a route that no longer exists.
|
|
233
|
-
SoftApTrace.stage(
|
|
234
|
-
"process_pinned_to_cellular",
|
|
235
|
-
"ok" to ok,
|
|
236
|
-
"network" to network,
|
|
237
|
-
"replaced" to (alreadyPinned?.toString() ?: "none"),
|
|
238
|
-
)
|
|
239
|
-
return@synchronized ok
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/** Undo [bindProcessToCellular]. Safe to call when nothing is pinned, and safe to call twice. */
|
|
243
|
-
fun unbindProcess() = synchronized(lock) {
|
|
244
|
-
if (closed) return@synchronized
|
|
245
|
-
val previous = synchronized(lock) { pinnedNetwork }
|
|
246
|
-
val pinned = synchronized(lock) {
|
|
247
|
-
processPinned.also {
|
|
248
|
-
processPinned = false
|
|
249
|
-
pinnedNetwork = null
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
if (!pinned) {
|
|
253
|
-
// The WHIP bind lifts the pin around itself, so "nothing was pinned" here means the
|
|
254
|
-
// sign-in never took one — which is the state in which that ServerSocket is fine and
|
|
255
|
-
// ACS's own sockets are not. Different bug, same-looking log without this line.
|
|
256
|
-
SoftApTrace.stage("process_unpinned", "ok" to true, "reason" to "nothing pinned")
|
|
257
|
-
return@synchronized
|
|
258
|
-
}
|
|
259
|
-
val ok = runCatching { connectivityManager()?.bindProcessToNetwork(null) }.isSuccess
|
|
260
|
-
// The default route the process falls back onto is the whole risk of unpinning: a leftover
|
|
261
|
-
// glasses AP here is what made the next `teams:create` fail with no internet.
|
|
262
|
-
SoftApTrace.stage(
|
|
263
|
-
"process_unpinned",
|
|
264
|
-
"ok" to ok,
|
|
265
|
-
"released" to (previous?.toString() ?: "unknown"),
|
|
266
|
-
"default" to defaultNetwork().toString(),
|
|
267
|
-
)
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Poll the app's default network until it is validated, bounded by [timeoutMs].
|
|
272
|
-
*
|
|
273
|
-
* Polled rather than awaited: what matters is which network *this app's* unbound sockets use,
|
|
274
|
-
* and that is `activeNetwork`, which has no per-app change callback.
|
|
275
|
-
*/
|
|
276
|
-
fun awaitValidatedDefault(timeoutMs: Long = DEFAULT_WAIT_MS): DefaultNetwork {
|
|
277
|
-
val deadline = SystemClock.elapsedRealtime() + timeoutMs
|
|
278
|
-
var current = defaultNetwork()
|
|
279
|
-
while (!current.usable && SystemClock.elapsedRealtime() < deadline) {
|
|
280
|
-
Thread.sleep(DEFAULT_POLL_MS)
|
|
281
|
-
current = defaultNetwork()
|
|
282
|
-
}
|
|
283
|
-
val fields = arrayOf(
|
|
284
|
-
"transport" to current.transport,
|
|
285
|
-
"validated" to current.validated,
|
|
286
|
-
"present" to current.present,
|
|
287
|
-
"glassesHotspot" to current.glassesHotspot,
|
|
288
|
-
)
|
|
289
|
-
// Error level when unusable so it survives a level filter: this is the line that explains an
|
|
290
|
-
// ACS join failing on a hotspot that was itself fine.
|
|
291
|
-
if (current.usable) {
|
|
292
|
-
SoftApTrace.stage("default_network_after_join", *fields)
|
|
293
|
-
} else {
|
|
294
|
-
SoftApTrace.failure("default_network_after_join", *fields)
|
|
295
|
-
}
|
|
296
|
-
return current
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/** The app's default network right now, without waiting. */
|
|
300
|
-
fun defaultNetwork(): DefaultNetwork {
|
|
301
|
-
val manager = connectivityManager() ?: return DefaultNetwork("unknown", false, false)
|
|
302
|
-
val active = manager.activeNetwork ?: return DefaultNetwork("none", false, false)
|
|
303
|
-
val capabilities =
|
|
304
|
-
manager.getNetworkCapabilities(active)
|
|
305
|
-
?: return DefaultNetwork("unknown", false, true)
|
|
306
|
-
val transport = transportName(capabilities)
|
|
307
|
-
val glassesHotspot =
|
|
308
|
-
transport == "wifi" && isGlassesHotspotDefault(active, manager.getLinkProperties(active))
|
|
309
|
-
return DefaultNetwork(
|
|
310
|
-
transport,
|
|
311
|
-
capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED),
|
|
312
|
-
true,
|
|
313
|
-
glassesHotspot,
|
|
314
|
-
)
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
private fun isGlassesHotspotDefault(network: Network, links: LinkProperties?): Boolean {
|
|
318
|
-
val ssid = currentWifiSsid()
|
|
319
|
-
if (isGlassesHotspotSsid(ssid)) {
|
|
320
|
-
SoftApTrace.stage("default_is_glasses_hotspot", "ssid" to ssid, "network" to network)
|
|
321
|
-
return true
|
|
322
|
-
}
|
|
323
|
-
val softAp = links?.linkAddresses?.any { address ->
|
|
324
|
-
val ip = address.address
|
|
325
|
-
ip is Inet4Address && isAndroidSoftApIpv4(ip.address)
|
|
326
|
-
} == true
|
|
327
|
-
if (softAp) {
|
|
328
|
-
SoftApTrace.stage(
|
|
329
|
-
"default_is_glasses_hotspot",
|
|
330
|
-
"ssid" to ssid.ifEmpty { "unknown" },
|
|
331
|
-
"reason" to "192.168.43.0/24",
|
|
332
|
-
"network" to network,
|
|
333
|
-
)
|
|
334
|
-
}
|
|
335
|
-
return softAp
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
private fun currentWifiSsid(): String {
|
|
339
|
-
val raw =
|
|
340
|
-
runCatching {
|
|
341
|
-
context.applicationContext.getSystemService(WifiManager::class.java)?.connectionInfo?.ssid
|
|
342
|
-
}.getOrNull()
|
|
343
|
-
.orEmpty()
|
|
344
|
-
return raw.trim().trim('"')
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
/**
|
|
348
|
-
* Drop the cellular pin only once the phone has a validated default internet of its own.
|
|
349
|
-
*
|
|
350
|
-
* Cancelling while the glasses hotspot is coming up used to unpin immediately. The AP was
|
|
351
|
-
* still broadcasting, Android treated it as the default Wi-Fi, and the next `teams:create`
|
|
352
|
-
* left the process over a network with no internet — which the wearer saw as
|
|
353
|
-
* `Request failed (503)` from Cloudflare, not as a hotspot problem.
|
|
354
|
-
*
|
|
355
|
-
* If the default route is still the leftover AP when the wait expires, the pin stays. The
|
|
356
|
-
* next sign-in refreshes it; unpinning onto SoftAP is the one thing we must not do.
|
|
357
|
-
*/
|
|
358
|
-
fun releaseWhenDefaultInternetReady(timeoutMs: Long = DEFAULT_WAIT_MS) {
|
|
359
|
-
val startedAt = SystemClock.elapsedRealtime()
|
|
360
|
-
SoftApTrace.stage(
|
|
361
|
-
"cellular_hold_release_requested",
|
|
362
|
-
"timeoutMs" to timeoutMs,
|
|
363
|
-
"pinned" to synchronized(lock) { processPinned },
|
|
364
|
-
)
|
|
365
|
-
val network = awaitValidatedDefault(timeoutMs)
|
|
366
|
-
if (network.usable) {
|
|
367
|
-
release()
|
|
368
|
-
SoftApTrace.stage(
|
|
369
|
-
"cellular_hold_release_done",
|
|
370
|
-
"waitedMs" to (SystemClock.elapsedRealtime() - startedAt),
|
|
371
|
-
"default" to network.toString(),
|
|
372
|
-
)
|
|
373
|
-
return
|
|
374
|
-
}
|
|
375
|
-
// Re-pin: a previous `leave()` may already have dropped the bind onto leftover Wi-Fi, and
|
|
376
|
-
// keeping a dead hold is not enough — the next `teams:create` still leaves through SoftAP
|
|
377
|
-
// and Cloudflare answers 503.
|
|
378
|
-
bindProcessToCellular()
|
|
379
|
-
SoftApTrace.failure(
|
|
380
|
-
"cellular_hold_kept",
|
|
381
|
-
"reason" to "default network is not usable internet; unpinning would strand the next request on SoftAP",
|
|
382
|
-
"transport" to network.transport,
|
|
383
|
-
"validated" to network.validated,
|
|
384
|
-
"glassesHotspot" to network.glassesHotspot,
|
|
385
|
-
)
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
/** Permanently retire a module-owned hold, including requests still waiting for validation. */
|
|
389
|
-
fun close() {
|
|
390
|
-
synchronized(lock) {
|
|
391
|
-
closed = true
|
|
392
|
-
releaseLocked(reason = "owner destroyed")
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
/** Drop the cellular request. Safe to call when nothing is held, and safe to call twice. */
|
|
397
|
-
fun release() {
|
|
398
|
-
synchronized(lock) { releaseLocked() }
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
/**
|
|
402
|
-
* @param reason why the hold is being dropped. `teardown` means a pin found here outlived the
|
|
403
|
-
* code that took it and is reported as a leak; a re-request drops the pin on purpose, and
|
|
404
|
-
* calling both the same thing would cry wolf on every second sign-in.
|
|
405
|
-
*/
|
|
406
|
-
private fun releaseLocked(reason: String = "teardown") {
|
|
407
|
-
// Backstop only: [bindProcessToCellular]'s caller unpins in a `finally`. Leaving the process
|
|
408
|
-
// pinned across a teardown would break the *next* call's WHIP listener, not this one's, which
|
|
409
|
-
// is exactly the kind of failure that is impossible to read from a log.
|
|
410
|
-
if (processPinned) {
|
|
411
|
-
val dropped = pinnedNetwork?.toString() ?: "unknown"
|
|
412
|
-
runCatching { connectivityManager()?.bindProcessToNetwork(null) }
|
|
413
|
-
processPinned = false
|
|
414
|
-
pinnedNetwork = null
|
|
415
|
-
if (reason == "teardown") {
|
|
416
|
-
// Reaching the backstop means the pin outlived the code that took it. That is the
|
|
417
|
-
// leak this class is most afraid of, so it is reported as a failure even though it
|
|
418
|
-
// has just been repaired: the next call would have inherited it.
|
|
419
|
-
SoftApTrace.failure("cellular_pin_leaked", "network" to dropped)
|
|
420
|
-
} else {
|
|
421
|
-
SoftApTrace.stage("cellular_pin_dropped", "network" to dropped, "reason" to reason)
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
val active = callback
|
|
425
|
-
if (active != null) {
|
|
426
|
-
runCatching { connectivityManager()?.unregisterNetworkCallback(active) }
|
|
427
|
-
}
|
|
428
|
-
callback = null
|
|
429
|
-
if (active != null) SoftApTrace.stage("cellular_hold_released")
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
private fun cellularIsValidated(manager: ConnectivityManager): Boolean =
|
|
433
|
-
findValidatedCellular(manager) != null
|
|
434
|
-
|
|
435
|
-
private fun findValidatedCellular(manager: ConnectivityManager): Network? =
|
|
436
|
-
manager.allNetworks.firstOrNull { network ->
|
|
437
|
-
val capabilities = manager.getNetworkCapabilities(network) ?: return@firstOrNull false
|
|
438
|
-
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) &&
|
|
439
|
-
isValidatedInternet(capabilities)
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
private fun connectivityManager(): ConnectivityManager? =
|
|
443
|
-
context.applicationContext.getSystemService(ConnectivityManager::class.java)
|
|
444
|
-
|
|
445
|
-
companion object {
|
|
446
|
-
/** Generous: failing here aborts a call the wearer asked for, on a phone that may be indoors. */
|
|
447
|
-
const val CELLULAR_WAIT_MS = 15_000L
|
|
448
|
-
|
|
449
|
-
/** Short: the default route normally settles in well under a second after the scoped join. */
|
|
450
|
-
const val DEFAULT_WAIT_MS = 8_000L
|
|
451
|
-
|
|
452
|
-
private const val DEFAULT_POLL_MS = 200L
|
|
453
|
-
|
|
454
|
-
fun isValidatedInternet(capabilities: NetworkCapabilities): Boolean =
|
|
455
|
-
capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
|
|
456
|
-
capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
|
457
|
-
|
|
458
|
-
/** For the trace only. Nothing routes by this name. */
|
|
459
|
-
fun transportName(capabilities: NetworkCapabilities): String =
|
|
460
|
-
transportNameOf(
|
|
461
|
-
cellular = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR),
|
|
462
|
-
wifi = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI),
|
|
463
|
-
vpn = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN),
|
|
464
|
-
ethernet = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET),
|
|
465
|
-
)
|
|
466
|
-
|
|
467
|
-
/**
|
|
468
|
-
* VPN first: when a tunnel owns the app's UID it is the transport that decides whether ACS
|
|
469
|
-
* reaches the internet, and it stacks on top of the others rather than replacing them.
|
|
470
|
-
*/
|
|
471
|
-
fun transportNameOf(
|
|
472
|
-
cellular: Boolean,
|
|
473
|
-
wifi: Boolean,
|
|
474
|
-
vpn: Boolean,
|
|
475
|
-
ethernet: Boolean,
|
|
476
|
-
): String =
|
|
477
|
-
when {
|
|
478
|
-
vpn -> "vpn"
|
|
479
|
-
cellular -> "cellular"
|
|
480
|
-
wifi -> "wifi"
|
|
481
|
-
ethernet -> "ethernet"
|
|
482
|
-
else -> "other"
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
/**
|
|
486
|
-
* Mentra Live SoftAP SSIDs look like `MentraLive_15f63c`. Android quotes them and, without
|
|
487
|
-
* location permission, reports `<unknown ssid>` instead — so SSID alone is not enough.
|
|
488
|
-
*/
|
|
489
|
-
fun isGlassesHotspotSsid(ssid: String): Boolean {
|
|
490
|
-
val normalized = ssid.trim().trim('"')
|
|
491
|
-
return normalized.startsWith("MentraLive", ignoreCase = true)
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
/** Android SoftAP's usual IPv4 LAN. Location-less `<unknown ssid>` still lands here. */
|
|
495
|
-
fun isAndroidSoftApIpv4(bytes: ByteArray): Boolean =
|
|
496
|
-
bytes.size == 4 &&
|
|
497
|
-
(bytes[0].toInt() and 0xff) == 192 &&
|
|
498
|
-
(bytes[1].toInt() and 0xff) == 168 &&
|
|
499
|
-
(bytes[2].toInt() and 0xff) == 43
|
|
500
|
-
}
|
|
501
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
package com.mentra.acsmeeting.network
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* An IPv4 subnet, taken from the phone's own address on a joined network.
|
|
5
|
-
*
|
|
6
|
-
* SoftAP correctness is an interface question, not an address-range question: the media path is
|
|
7
|
-
* right when the selected candidate sits on the network we joined, and wrong otherwise. Matching
|
|
8
|
-
* "looks RFC1918" or "starts with 192.168.43" answers a different question and stops being true the
|
|
9
|
-
* moment an OEM picks another hotspot subnet, so every SoftAP check compares against the prefix
|
|
10
|
-
* `LinkProperties` actually reported.
|
|
11
|
-
*/
|
|
12
|
-
data class Ipv4Prefix(val address: String, val prefixLength: Int) {
|
|
13
|
-
|
|
14
|
-
private val network: Int? = packIpv4(address)?.let { it and maskFor(prefixLength) }
|
|
15
|
-
|
|
16
|
-
/** True when [candidate] is a dotted-quad IPv4 address inside this prefix. */
|
|
17
|
-
fun contains(candidate: String?): Boolean {
|
|
18
|
-
val base = network ?: return false
|
|
19
|
-
val value = packIpv4(candidate) ?: return false
|
|
20
|
-
return (value and maskFor(prefixLength)) == base
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
override fun toString(): String = "$address/$prefixLength"
|
|
24
|
-
|
|
25
|
-
companion object {
|
|
26
|
-
/**
|
|
27
|
-
* Dotted-quad to a packed int, or null when [token] is not one. Deliberately strict: a
|
|
28
|
-
* token like `192.168.43` or `candidate:1` must not be read as an address, because the
|
|
29
|
-
* callers scan whitespace-split SDP lines where most tokens are not addresses at all.
|
|
30
|
-
*/
|
|
31
|
-
fun packIpv4(token: String?): Int? {
|
|
32
|
-
val text = token ?: return null
|
|
33
|
-
val octets = text.split('.')
|
|
34
|
-
if (octets.size != 4) return null
|
|
35
|
-
var packed = 0
|
|
36
|
-
for (octet in octets) {
|
|
37
|
-
if (octet.isEmpty() || octet.length > 3) return null
|
|
38
|
-
val value = octet.toIntOrNull() ?: return null
|
|
39
|
-
if (value < 0 || value > 255) return null
|
|
40
|
-
packed = (packed shl 8) or value
|
|
41
|
-
}
|
|
42
|
-
return packed
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** A /0 masks nothing; Kotlin's `shl 32` is a no-op, so that case is spelled out. */
|
|
46
|
-
private fun maskFor(prefixLength: Int): Int {
|
|
47
|
-
val bits = prefixLength.coerceIn(0, 32)
|
|
48
|
-
return if (bits == 0) 0 else (-1 shl (32 - bits))
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
package com.mentra.acsmeeting.network
|
|
2
|
-
|
|
3
|
-
import java.net.Inet4Address
|
|
4
|
-
import java.net.NetworkInterface
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* The kernel's interface table, as libwebrtc's `getifaddrs` sees it.
|
|
8
|
-
*
|
|
9
|
-
* Every SoftAP ICE failure so far has come from our view of the network disagreeing with
|
|
10
|
-
* libwebrtc's, and the two views come from different sources. We publish
|
|
11
|
-
* [ConnectivityManager.getLinkProperties][android.net.ConnectivityManager.getLinkProperties] for
|
|
12
|
-
* the scoped `Network`; libwebrtc enumerates `getifaddrs` and only asks our monitor for each
|
|
13
|
-
* *interface name*'s adapter type. When those disagree — an address on a name we never typed, or a
|
|
14
|
-
* scoped address absent from the table entirely — no mask and no inventory can help, because ICE
|
|
15
|
-
* gathers per address, not per `Network`.
|
|
16
|
-
*
|
|
17
|
-
* `java.net.NetworkInterface` is getifaddrs-backed, so this is the closest thing to libwebrtc's
|
|
18
|
-
* own input that we can read from Kotlin. Pure enough to unit test, which matters: it is the
|
|
19
|
-
* evidence the next diagnosis rests on.
|
|
20
|
-
*/
|
|
21
|
-
object NetworkFacts {
|
|
22
|
-
|
|
23
|
-
/** One interface, with the flags libwebrtc's enumeration also keys off. */
|
|
24
|
-
data class Iface(
|
|
25
|
-
val name: String,
|
|
26
|
-
val index: Int,
|
|
27
|
-
val up: Boolean,
|
|
28
|
-
val loopback: Boolean,
|
|
29
|
-
val virtual: Boolean,
|
|
30
|
-
val pointToPoint: Boolean,
|
|
31
|
-
val mtu: Int,
|
|
32
|
-
/** IPv4 only, as `address/prefixLength` — the form libwebrtc turns into a Network key. */
|
|
33
|
-
val ipv4: List<String>,
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
/** Read the table. Never throws: a diagnostic that crashes the call is worse than no data. */
|
|
37
|
-
fun snapshot(): List<Iface> =
|
|
38
|
-
runCatching {
|
|
39
|
-
NetworkInterface.getNetworkInterfaces()?.toList().orEmpty().map { candidate ->
|
|
40
|
-
Iface(
|
|
41
|
-
name = candidate.name ?: "?",
|
|
42
|
-
index = runCatching { candidate.index }.getOrDefault(-1),
|
|
43
|
-
up = runCatching { candidate.isUp }.getOrDefault(false),
|
|
44
|
-
loopback = runCatching { candidate.isLoopback }.getOrDefault(false),
|
|
45
|
-
virtual = runCatching { candidate.isVirtual }.getOrDefault(false),
|
|
46
|
-
pointToPoint = runCatching { candidate.isPointToPoint }.getOrDefault(false),
|
|
47
|
-
mtu = runCatching { candidate.mtu }.getOrDefault(-1),
|
|
48
|
-
ipv4 =
|
|
49
|
-
runCatching {
|
|
50
|
-
candidate.interfaceAddresses
|
|
51
|
-
.filter { it.address is Inet4Address }
|
|
52
|
-
.map { "${it.address.hostAddress}/${it.networkPrefixLength}" }
|
|
53
|
-
}.getOrDefault(emptyList()),
|
|
54
|
-
)
|
|
55
|
-
}
|
|
56
|
-
}.getOrElse { emptyList() }
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Only interfaces that can carry a host candidate: up, non-loopback, with an IPv4 address.
|
|
60
|
-
* Anything here is something ICE may gather on.
|
|
61
|
-
*/
|
|
62
|
-
fun gatherable(table: List<Iface> = snapshot()): List<Iface> =
|
|
63
|
-
table.filter { it.up && !it.loopback && it.ipv4.isNotEmpty() }
|
|
64
|
-
|
|
65
|
-
/** The interface the kernel says owns [address], or null when the table has never seen it. */
|
|
66
|
-
fun ownerOf(address: String, table: List<Iface> = snapshot()): String? =
|
|
67
|
-
table.firstOrNull { iface -> iface.ipv4.any { it.substringBefore('/') == address } }?.name
|
|
68
|
-
|
|
69
|
-
/** One log line per interface, flags abbreviated so a full table fits in a logcat entry. */
|
|
70
|
-
fun render(table: List<Iface>): String =
|
|
71
|
-
table.joinToString(" ") { iface ->
|
|
72
|
-
val flags =
|
|
73
|
-
buildString {
|
|
74
|
-
if (iface.up) append("U")
|
|
75
|
-
if (iface.loopback) append("L")
|
|
76
|
-
if (iface.virtual) append("V")
|
|
77
|
-
if (iface.pointToPoint) append("P")
|
|
78
|
-
}
|
|
79
|
-
"${iface.name}#${iface.index}[$flags mtu=${iface.mtu}](${iface.ipv4.joinToString(",")})"
|
|
80
|
-
}
|
|
81
|
-
}
|