@mentra/acs-meeting 3.2.0-dev.221 → 3.2.0-dev.223
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 +4 -2
- package/android/build.gradle +23 -0
- package/android/src/androidTest/java/com/mentra/acsmeeting/network/SoftApFeasibilityGateTest.kt +291 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/mentra/acsmeeting/AbandonedCallAgent.kt +36 -0
- package/android/src/main/java/com/mentra/acsmeeting/AcsMeetingModule.kt +392 -22
- package/android/src/main/java/com/mentra/acsmeeting/AcsMeetingSession.kt +909 -56
- package/android/src/main/java/com/mentra/acsmeeting/CallCapabilities.kt +33 -0
- package/android/src/main/java/com/mentra/acsmeeting/audio/AcsAudioPolicy.kt +21 -0
- package/android/src/main/java/com/mentra/acsmeeting/audio/AudioUplinkChain.kt +126 -0
- package/android/src/main/java/com/mentra/acsmeeting/audio/PcmBridge.kt +27 -4
- package/android/src/main/java/com/mentra/acsmeeting/audio/UplinkPacer.kt +45 -10
- package/android/src/main/java/com/mentra/acsmeeting/audio/UplinkSender.kt +110 -5
- package/android/src/main/java/com/mentra/acsmeeting/network/InternetHold.kt +501 -0
- package/android/src/main/java/com/mentra/acsmeeting/network/Ipv4Prefix.kt +51 -0
- package/android/src/main/java/com/mentra/acsmeeting/network/NetworkFacts.kt +81 -0
- package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkChangeDetector.kt +355 -0
- package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkError.kt +109 -0
- package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkObserver.kt +160 -0
- package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkReadiness.kt +103 -0
- package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkRequestSpec.kt +53 -0
- package/android/src/main/java/com/mentra/acsmeeting/network/ScopedNetworkState.kt +127 -0
- package/android/src/main/java/com/mentra/acsmeeting/network/ScopedSoftApNetwork.kt +573 -0
- package/android/src/main/java/com/mentra/acsmeeting/source/CloudflareWhepSource.kt +10 -101
- package/android/src/main/java/com/mentra/acsmeeting/source/DecodedTrackRelay.kt +147 -0
- package/android/src/main/java/com/mentra/acsmeeting/source/GlassesMediaSource.kt +47 -5
- package/android/src/main/java/com/mentra/acsmeeting/source/LocalWhipIngestSource.kt +772 -0
- package/android/src/main/java/com/mentra/acsmeeting/source/MeetingVideoSourceSpec.kt +73 -0
- package/android/src/main/java/com/mentra/acsmeeting/source/SdpAdapter.kt +17 -0
- package/android/src/main/java/com/mentra/acsmeeting/source/SelectedIcePair.kt +139 -0
- package/android/src/main/java/com/mentra/acsmeeting/source/SoftApIceDiagnosis.kt +144 -0
- package/android/src/main/java/com/mentra/acsmeeting/source/SoftApIcePolicy.kt +46 -0
- package/android/src/main/java/com/mentra/acsmeeting/source/SoftApSdpGuard.kt +160 -0
- package/android/src/main/java/com/mentra/acsmeeting/source/TrackRegistry.kt +3 -1
- package/android/src/main/java/com/mentra/acsmeeting/source/VideoSourceArm.kt +69 -0
- package/android/src/main/java/com/mentra/acsmeeting/source/WhipIngestProtocol.kt +207 -0
- package/android/src/main/java/com/mentra/acsmeeting/source/WhipIngestServer.kt +356 -0
- package/android/src/main/java/com/mentra/acsmeeting/telemetry/AvSyncProbe.kt +155 -0
- package/android/src/main/java/com/mentra/acsmeeting/telemetry/PipelineStats.kt +94 -3
- package/android/src/main/java/com/mentra/acsmeeting/telemetry/PipelineTicker.kt +33 -5
- package/android/src/main/java/com/mentra/acsmeeting/telemetry/RingPercentile.kt +15 -0
- package/android/src/main/java/com/mentra/acsmeeting/telemetry/VideoQuality.kt +178 -0
- package/android/src/main/java/com/mentra/acsmeeting/telemetry/VideoRateVerdict.kt +145 -0
- package/android/src/main/java/com/mentra/acsmeeting/trace/SoftApTrace.kt +143 -0
- package/android/src/main/java/com/mentra/acsmeeting/video/AcsFrameSender.kt +23 -4
- package/android/src/main/java/com/mentra/acsmeeting/video/FramePacer.kt +70 -0
- package/android/src/main/java/com/mentra/acsmeeting/video/I420FormatSpec.kt +1 -1
- package/android/src/main/java/com/mentra/acsmeeting/video/VideoProfile.kt +36 -3
- package/android/src/test/java/com/mentra/acsmeeting/AbandonedCallAgentTest.kt +74 -0
- package/android/src/test/java/com/mentra/acsmeeting/CallCapabilitiesTest.kt +47 -0
- package/android/src/test/java/com/mentra/acsmeeting/audio/AudioUplinkChainTest.kt +250 -0
- package/android/src/test/java/com/mentra/acsmeeting/audio/CapturePolicyTest.kt +44 -0
- package/android/src/test/java/com/mentra/acsmeeting/audio/UplinkPacerTest.kt +34 -4
- package/android/src/test/java/com/mentra/acsmeeting/audio/UplinkSenderTest.kt +96 -0
- package/android/src/test/java/com/mentra/acsmeeting/network/InternetHoldLifecycleTest.kt +102 -0
- package/android/src/test/java/com/mentra/acsmeeting/network/InternetHoldTest.kt +148 -0
- package/android/src/test/java/com/mentra/acsmeeting/network/Ipv4PrefixTest.kt +59 -0
- package/android/src/test/java/com/mentra/acsmeeting/network/NetworkFactsTest.kt +59 -0
- package/android/src/test/java/com/mentra/acsmeeting/network/ScopedLocalNetworkPermissionTest.kt +35 -0
- package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkChangeDetectorTest.kt +165 -0
- package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkErrorTest.kt +96 -0
- package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkLifecycleTest.kt +187 -0
- package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkObserverTest.kt +172 -0
- package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkReadinessTest.kt +141 -0
- package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkRequestSpecTest.kt +69 -0
- package/android/src/test/java/com/mentra/acsmeeting/source/AcsInvestigationTest.kt +40 -0
- package/android/src/test/java/com/mentra/acsmeeting/source/DecodedTrackRelayTest.kt +41 -0
- package/android/src/test/java/com/mentra/acsmeeting/source/GlassesMediaSourceTest.kt +26 -8
- package/android/src/test/java/com/mentra/acsmeeting/source/MeetingVideoSourceSpecTest.kt +109 -0
- package/android/src/test/java/com/mentra/acsmeeting/source/SelectedIcePairTest.kt +165 -0
- package/android/src/test/java/com/mentra/acsmeeting/source/SoftApIceDiagnosisTest.kt +148 -0
- package/android/src/test/java/com/mentra/acsmeeting/source/SoftApIcePolicyTest.kt +76 -0
- package/android/src/test/java/com/mentra/acsmeeting/source/SoftApSdpGuardTest.kt +275 -0
- package/android/src/test/java/com/mentra/acsmeeting/source/TrackRegistryTest.kt +9 -0
- package/android/src/test/java/com/mentra/acsmeeting/source/WhipIngestProtocolTest.kt +356 -0
- package/android/src/test/java/com/mentra/acsmeeting/source/WhipIngestServerLoopbackTest.kt +411 -0
- package/android/src/test/java/com/mentra/acsmeeting/telemetry/AvSyncProbeTest.kt +83 -0
- package/android/src/test/java/com/mentra/acsmeeting/telemetry/PipelineStatsTest.kt +69 -3
- package/android/src/test/java/com/mentra/acsmeeting/telemetry/VideoQualityTest.kt +182 -0
- package/android/src/test/java/com/mentra/acsmeeting/telemetry/VideoRateVerdictTest.kt +149 -0
- package/android/src/test/java/com/mentra/acsmeeting/trace/SoftApTraceTest.kt +97 -0
- package/android/src/test/java/com/mentra/acsmeeting/video/AcsTimestampTest.kt +34 -0
- package/android/src/test/java/com/mentra/acsmeeting/video/FramePacerTest.kt +105 -0
- package/android/src/test/java/com/mentra/acsmeeting/video/I420FormatSpecTest.kt +5 -0
- package/android/src/test/java/com/mentra/acsmeeting/video/VideoProfileTest.kt +38 -3
- package/build/AcsMeeting.types.d.ts +5 -0
- package/build/AcsMeeting.types.d.ts.map +1 -1
- package/build/AcsMeeting.types.js.map +1 -1
- package/build/AcsMeetingModule.d.ts +23 -0
- package/build/AcsMeetingModule.d.ts.map +1 -1
- package/build/AcsMeetingModule.js.map +1 -1
- package/build/AcsMeetingModule.web.d.ts +15 -0
- package/build/AcsMeetingModule.web.d.ts.map +1 -1
- package/build/AcsMeetingModule.web.js +13 -0
- package/build/AcsMeetingModule.web.js.map +1 -1
- package/ios/AcsMeetingModule.swift +213 -2
- package/package.json +1 -1
- package/src/AcsMeeting.types.ts +5 -0
- package/src/AcsMeetingModule.ts +13 -0
- package/src/AcsMeetingModule.web.ts +13 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
package com.mentra.acsmeeting.network
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.assertEquals
|
|
4
|
+
import org.junit.Assert.assertFalse
|
|
5
|
+
import org.junit.Assert.assertTrue
|
|
6
|
+
import org.junit.Test
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Covers the parts of [InternetHold] that decide something, which is the verdict on a default
|
|
10
|
+
* network and the name that verdict is reported under. The framework plumbing around them
|
|
11
|
+
* (`requestNetwork`, `activeNetwork`) is not reachable from a plain JVM test and is not what went
|
|
12
|
+
* wrong on device: the failure was proceeding on an unvalidated route, and that judgement lives
|
|
13
|
+
* here.
|
|
14
|
+
*/
|
|
15
|
+
class InternetHoldTest {
|
|
16
|
+
|
|
17
|
+
@Test
|
|
18
|
+
fun `a present validated network is the only usable one`() {
|
|
19
|
+
assertTrue(InternetHold.DefaultNetwork("cellular", validated = true, present = true).usable)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@Test
|
|
23
|
+
fun `a network that is up but not validated is not usable`() {
|
|
24
|
+
// The device failure exactly: cellular present, carrying no TLS, and ACS joining over it.
|
|
25
|
+
val cellular = InternetHold.DefaultNetwork("cellular", validated = false, present = true)
|
|
26
|
+
assertFalse(cellular.usable)
|
|
27
|
+
assertEquals("cellular (unvalidated)", cellular.toString())
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@Test
|
|
31
|
+
fun `no default network at all is reported as none rather than as an unvalidated one`() {
|
|
32
|
+
// Distinct causes need distinct copy: "no network" is airplane mode, "unvalidated" is a
|
|
33
|
+
// radio that is up and not working yet.
|
|
34
|
+
val absent = InternetHold.DefaultNetwork("none", validated = false, present = false)
|
|
35
|
+
assertFalse(absent.usable)
|
|
36
|
+
assertEquals("none", absent.toString())
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@Test
|
|
40
|
+
fun `a validated network cannot be usable while absent`() {
|
|
41
|
+
// Guards against reading a stale capability snapshot as a live route.
|
|
42
|
+
assertFalse(InternetHold.DefaultNetwork("cellular", validated = true, present = false).usable)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@Test
|
|
46
|
+
fun `a validated leftover glasses hotspot is not usable internet`() {
|
|
47
|
+
// The 17:52:24 failure: Android marked MentraLive_* VALIDATED, teardown dropped the
|
|
48
|
+
// cellular pin, and the next teams:create left through SoftAP. Cloudflare answered 503.
|
|
49
|
+
val leftover =
|
|
50
|
+
InternetHold.DefaultNetwork("wifi", validated = true, present = true, glassesHotspot = true)
|
|
51
|
+
assertFalse(leftover.usable)
|
|
52
|
+
assertEquals("wifi (validated, leftover-hotspot)", leftover.toString())
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@Test
|
|
56
|
+
fun `a validated cellular network renders with its transport and state`() {
|
|
57
|
+
assertEquals(
|
|
58
|
+
"cellular (validated)",
|
|
59
|
+
InternetHold.DefaultNetwork("cellular", validated = true, present = true).toString(),
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@Test
|
|
64
|
+
fun `Mentra Live SoftAP SSIDs are leftover glasses hotspots`() {
|
|
65
|
+
assertTrue(InternetHold.isGlassesHotspotSsid("MentraLive_15f63c"))
|
|
66
|
+
assertTrue(InternetHold.isGlassesHotspotSsid("\"MentraLive_15f63c\""))
|
|
67
|
+
assertTrue(InternetHold.isGlassesHotspotSsid("mentralive_aabbcc"))
|
|
68
|
+
assertFalse(InternetHold.isGlassesHotspotSsid("<unknown ssid>"))
|
|
69
|
+
assertFalse(InternetHold.isGlassesHotspotSsid("Office-WiFi"))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@Test
|
|
73
|
+
fun `Android SoftAP IPv4 is the leftover glasses LAN`() {
|
|
74
|
+
assertTrue(InternetHold.isAndroidSoftApIpv4(byteArrayOf(192.toByte(), 168.toByte(), 43, 154.toByte())))
|
|
75
|
+
assertFalse(InternetHold.isAndroidSoftApIpv4(byteArrayOf(192.toByte(), 168.toByte(), 1, 10)))
|
|
76
|
+
assertFalse(InternetHold.isAndroidSoftApIpv4(byteArrayOf(10, 0, 0, 1)))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@Test
|
|
80
|
+
fun `a vpn is named ahead of the transport it runs over`() {
|
|
81
|
+
// A tunnel owning the app's UID is what decides whether ACS reaches the internet, and it
|
|
82
|
+
// stacks on cellular rather than replacing it. Naming the carrier would hide the cause of
|
|
83
|
+
// the EPERM failures we chased.
|
|
84
|
+
assertEquals(
|
|
85
|
+
"vpn",
|
|
86
|
+
InternetHold.transportNameOf(cellular = true, wifi = false, vpn = true, ethernet = false),
|
|
87
|
+
)
|
|
88
|
+
assertEquals(
|
|
89
|
+
"vpn",
|
|
90
|
+
InternetHold.transportNameOf(cellular = false, wifi = true, vpn = true, ethernet = false),
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@Test
|
|
95
|
+
fun `each transport has a name and an unknown mix does not crash the trace`() {
|
|
96
|
+
assertEquals(
|
|
97
|
+
"cellular",
|
|
98
|
+
InternetHold.transportNameOf(cellular = true, wifi = false, vpn = false, ethernet = false),
|
|
99
|
+
)
|
|
100
|
+
assertEquals(
|
|
101
|
+
"wifi",
|
|
102
|
+
InternetHold.transportNameOf(cellular = false, wifi = true, vpn = false, ethernet = false),
|
|
103
|
+
)
|
|
104
|
+
assertEquals(
|
|
105
|
+
"ethernet",
|
|
106
|
+
InternetHold.transportNameOf(cellular = false, wifi = false, vpn = false, ethernet = true),
|
|
107
|
+
)
|
|
108
|
+
assertEquals(
|
|
109
|
+
"other",
|
|
110
|
+
InternetHold.transportNameOf(cellular = false, wifi = false, vpn = false, ethernet = false),
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@Test
|
|
115
|
+
fun `cellular is named over wifi when both are present`() {
|
|
116
|
+
// Only reached when the app's default has both transports; cellular is the one this class
|
|
117
|
+
// holds up, so it is the one worth naming.
|
|
118
|
+
assertEquals(
|
|
119
|
+
"cellular",
|
|
120
|
+
InternetHold.transportNameOf(cellular = true, wifi = true, vpn = false, ethernet = false),
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@Test
|
|
125
|
+
fun `a hold that could not be taken reports neither held nor validated`() {
|
|
126
|
+
val none = InternetHold.CellularHold(held = false, validated = false, waitedMs = 0)
|
|
127
|
+
assertFalse(none.held)
|
|
128
|
+
assertFalse(none.validated)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@Test
|
|
132
|
+
fun `a held request that never validated is still recorded as held`() {
|
|
133
|
+
// The request is deliberately kept after a validation timeout, so that a caller which
|
|
134
|
+
// continues anyway does not also lose the radio. Losing this distinction would make the
|
|
135
|
+
// release path look unnecessary.
|
|
136
|
+
val held = InternetHold.CellularHold(held = true, validated = false, waitedMs = 15_000)
|
|
137
|
+
assertTrue(held.held)
|
|
138
|
+
assertFalse(held.validated)
|
|
139
|
+
assertEquals(15_000, held.waitedMs)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@Test
|
|
143
|
+
fun `the cellular wait is longer than the default-route wait`() {
|
|
144
|
+
// Bringing a radio up from cold is slow; a default route swapping to a network already up
|
|
145
|
+
// is not. Equal budgets would either abort good calls or stall every one of them.
|
|
146
|
+
assertTrue(InternetHold.CELLULAR_WAIT_MS > InternetHold.DEFAULT_WAIT_MS)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
package com.mentra.acsmeeting.network
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.assertEquals
|
|
4
|
+
import org.junit.Assert.assertFalse
|
|
5
|
+
import org.junit.Assert.assertNull
|
|
6
|
+
import org.junit.Assert.assertTrue
|
|
7
|
+
import org.junit.Test
|
|
8
|
+
|
|
9
|
+
class Ipv4PrefixTest {
|
|
10
|
+
|
|
11
|
+
@Test
|
|
12
|
+
fun `contains the phone and the gateway on a 24`() {
|
|
13
|
+
val prefix = Ipv4Prefix("192.168.43.20", 24)
|
|
14
|
+
assertTrue(prefix.contains("192.168.43.1"))
|
|
15
|
+
assertTrue(prefix.contains("192.168.43.20"))
|
|
16
|
+
assertTrue(prefix.contains("192.168.43.255"))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The whole point: a cellular 10/8 address is RFC1918 and still the wrong interface. */
|
|
20
|
+
@Test
|
|
21
|
+
fun `rejects a private address on another interface`() {
|
|
22
|
+
val prefix = Ipv4Prefix("192.168.43.20", 24)
|
|
23
|
+
assertFalse(prefix.contains("10.171.36.4"))
|
|
24
|
+
assertFalse(prefix.contains("192.168.1.20"))
|
|
25
|
+
assertFalse(prefix.contains("172.16.0.1"))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** An OEM hotspot on another subnet must still pass, which is why /24 is never assumed. */
|
|
29
|
+
@Test
|
|
30
|
+
fun `honours the reported prefix length`() {
|
|
31
|
+
assertTrue(Ipv4Prefix("192.168.49.1", 16).contains("192.168.43.7"))
|
|
32
|
+
assertFalse(Ipv4Prefix("192.168.49.1", 24).contains("192.168.43.7"))
|
|
33
|
+
assertTrue(Ipv4Prefix("10.5.4.3", 8).contains("10.99.99.99"))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@Test
|
|
37
|
+
fun `non-addresses are not addresses`() {
|
|
38
|
+
val prefix = Ipv4Prefix("192.168.43.20", 24)
|
|
39
|
+
assertFalse(prefix.contains(null))
|
|
40
|
+
assertFalse(prefix.contains("typ"))
|
|
41
|
+
assertFalse(prefix.contains("192.168.43"))
|
|
42
|
+
assertFalse(prefix.contains("192.168.43.256"))
|
|
43
|
+
assertFalse(prefix.contains("192.168.43.20.1"))
|
|
44
|
+
assertFalse(prefix.contains("a4f1c2d3.local"))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@Test
|
|
48
|
+
fun `packs and prints`() {
|
|
49
|
+
assertEquals(0x0A000001, Ipv4Prefix.packIpv4("10.0.0.1"))
|
|
50
|
+
assertNull(Ipv4Prefix.packIpv4("not.an.ip.addr"))
|
|
51
|
+
assertEquals("192.168.43.20/24", Ipv4Prefix("192.168.43.20", 24).toString())
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** An unparseable own-address must match nothing rather than everything. */
|
|
55
|
+
@Test
|
|
56
|
+
fun `a malformed prefix contains nothing`() {
|
|
57
|
+
assertFalse(Ipv4Prefix("garbage", 24).contains("192.168.43.1"))
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
package com.mentra.acsmeeting.network
|
|
2
|
+
|
|
3
|
+
import com.mentra.acsmeeting.network.NetworkFacts.Iface
|
|
4
|
+
import org.assertj.core.api.Assertions.assertThat
|
|
5
|
+
import org.junit.Test
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The attribution helpers decide what the next SoftAP diagnosis says, so they are pinned rather
|
|
9
|
+
* than eyeballed in a log line.
|
|
10
|
+
*/
|
|
11
|
+
class NetworkFactsTest {
|
|
12
|
+
|
|
13
|
+
private fun iface(
|
|
14
|
+
name: String,
|
|
15
|
+
ipv4: List<String>,
|
|
16
|
+
up: Boolean = true,
|
|
17
|
+
loopback: Boolean = false,
|
|
18
|
+
) = Iface(name, 1, up, loopback, false, false, 1500, ipv4)
|
|
19
|
+
|
|
20
|
+
private val table =
|
|
21
|
+
listOf(
|
|
22
|
+
iface("lo", listOf("127.0.0.1/8"), loopback = true),
|
|
23
|
+
iface("wlan0", listOf("192.168.43.79/24")),
|
|
24
|
+
iface("rmnet_data0", listOf("10.48.51.202/30")),
|
|
25
|
+
iface("dummy0", emptyList()),
|
|
26
|
+
iface("epdg0", listOf("10.9.9.9/32"), up = false),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
@Test
|
|
30
|
+
fun `the owner of an address is the interface the table says carries it`() {
|
|
31
|
+
assertThat(NetworkFacts.ownerOf("192.168.43.79", table)).isEqualTo("wlan0")
|
|
32
|
+
assertThat(NetworkFacts.ownerOf("10.48.51.202", table)).isEqualTo("rmnet_data0")
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** An address ICE gathered that is absent here means our two views of the network disagree. */
|
|
36
|
+
@Test
|
|
37
|
+
fun `an address the table has never seen has no owner`() {
|
|
38
|
+
assertThat(NetworkFacts.ownerOf("192.168.50.7", table)).isNull()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@Test
|
|
42
|
+
fun `a prefix is not mistaken for the address`() {
|
|
43
|
+
assertThat(NetworkFacts.ownerOf("192.168.43.79/24", table)).isNull()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@Test
|
|
47
|
+
fun `gatherable drops loopback, down, and addressless interfaces`() {
|
|
48
|
+
assertThat(NetworkFacts.gatherable(table).map { it.name })
|
|
49
|
+
.containsExactly("wlan0", "rmnet_data0")
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@Test
|
|
53
|
+
fun `render keeps the name, flags and every address`() {
|
|
54
|
+
val line = NetworkFacts.render(listOf(iface("wlan0", listOf("192.168.43.79/24", "10.1.2.3/24"))))
|
|
55
|
+
|
|
56
|
+
assertThat(line).contains("wlan0#1")
|
|
57
|
+
assertThat(line).contains("192.168.43.79/24,10.1.2.3/24")
|
|
58
|
+
}
|
|
59
|
+
}
|
package/android/src/test/java/com/mentra/acsmeeting/network/ScopedLocalNetworkPermissionTest.kt
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
package com.mentra.acsmeeting.network
|
|
2
|
+
|
|
3
|
+
import org.assertj.core.api.Assertions.assertThat
|
|
4
|
+
import org.junit.Test
|
|
5
|
+
|
|
6
|
+
class ScopedLocalNetworkPermissionTest {
|
|
7
|
+
@Test
|
|
8
|
+
fun `Android 17 preserves implicit access for the SDK 36 host`() {
|
|
9
|
+
assertThat(ScopedSoftApNetwork.hasLocalNetworkPermission(37, 36) {
|
|
10
|
+
error("Legacy targets must not query the unneeded runtime grant")
|
|
11
|
+
}).isTrue()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@Test
|
|
15
|
+
fun `older Android does not query a permission it cannot enforce`() {
|
|
16
|
+
assertThat(ScopedSoftApNetwork.hasLocalNetworkPermission(36, 37) {
|
|
17
|
+
error("Older Android must not query the new permission")
|
|
18
|
+
}).isTrue()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@Test
|
|
22
|
+
fun `Android 17 denies a target 37 host without the grant`() {
|
|
23
|
+
assertThat(ScopedSoftApNetwork.hasLocalNetworkPermission(37, 37) { false }).isFalse()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@Test
|
|
27
|
+
fun `Android 17 allows a target 37 host with the grant`() {
|
|
28
|
+
assertThat(ScopedSoftApNetwork.hasLocalNetworkPermission(37, 37) { true }).isTrue()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@Test
|
|
32
|
+
fun `future Android and target versions still enforce the grant`() {
|
|
33
|
+
assertThat(ScopedSoftApNetwork.hasLocalNetworkPermission(38, 38) { false }).isFalse()
|
|
34
|
+
}
|
|
35
|
+
}
|
package/android/src/test/java/com/mentra/acsmeeting/network/ScopedNetworkChangeDetectorTest.kt
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
package com.mentra.acsmeeting.network
|
|
2
|
+
|
|
3
|
+
import com.mentra.acsmeeting.network.ScopedNetworkChangeDetector.Companion.formatIpv4
|
|
4
|
+
import com.mentra.acsmeeting.network.ScopedNetworkChangeDetector.Companion.mergeScopedNetwork
|
|
5
|
+
import com.mentra.acsmeeting.network.ScopedNetworkChangeDetector.Companion.renderInventory
|
|
6
|
+
import com.mentra.acsmeeting.network.ScopedNetworkChangeDetector.Companion.toNetworkInformation
|
|
7
|
+
import com.mentra.acsmeeting.network.ScopedNetworkChangeDetector.ScopedInterface
|
|
8
|
+
import com.mentra.acsmeeting.source.SoftApIcePolicy
|
|
9
|
+
import org.webrtc.PeerConnectionFactory
|
|
10
|
+
import org.assertj.core.api.Assertions.assertThat
|
|
11
|
+
import org.junit.Test
|
|
12
|
+
import org.webrtc.NetworkChangeDetector
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The merge is the part that decides whether ICE can see the hotspot at all, so it is tested
|
|
16
|
+
* directly rather than only through the on-device gate.
|
|
17
|
+
*/
|
|
18
|
+
class ScopedNetworkChangeDetectorTest {
|
|
19
|
+
|
|
20
|
+
private fun ipv4(a: Int, b: Int, c: Int, d: Int) =
|
|
21
|
+
byteArrayOf(a.toByte(), b.toByte(), c.toByte(), d.toByte())
|
|
22
|
+
|
|
23
|
+
private fun softAp(name: String = "wlan1", handle: Long = 42L) =
|
|
24
|
+
ScopedInterface(name, handle, listOf(ipv4(192, 168, 43, 20)))
|
|
25
|
+
|
|
26
|
+
private fun detected(name: String) =
|
|
27
|
+
NetworkChangeDetector.NetworkInformation(
|
|
28
|
+
name,
|
|
29
|
+
NetworkChangeDetector.ConnectionType.CONNECTION_4G,
|
|
30
|
+
NetworkChangeDetector.ConnectionType.CONNECTION_NONE,
|
|
31
|
+
7L,
|
|
32
|
+
arrayOf(NetworkChangeDetector.IPAddress(ipv4(10, 8, 0, 2))),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
@Test
|
|
36
|
+
fun `the scoped SoftAP is the only Wi-Fi in the inventory`() {
|
|
37
|
+
val merged = mergeScopedNetwork(listOf(detected("rmnet0")), softAp())
|
|
38
|
+
|
|
39
|
+
assertThat(merged.map { it.name }).containsExactly("wlan1")
|
|
40
|
+
assertThat(merged.single().ipAddresses[0].address).isEqualTo(ipv4(192, 168, 43, 20))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The collision that produced `count=0` candidates on device when two entries shared handle 0.
|
|
45
|
+
* One entry with the real handle cannot collide with itself.
|
|
46
|
+
*/
|
|
47
|
+
@Test
|
|
48
|
+
fun `the inventory is a single entry, so no two entries can share a handle`() {
|
|
49
|
+
val merged = mergeScopedNetwork(listOf(detected("rmnet_data0"), detected("wlan0")), softAp())
|
|
50
|
+
|
|
51
|
+
assertThat(merged).hasSize(1)
|
|
52
|
+
assertThat(merged.map { it.handle }.distinct()).hasSize(1)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The mask has to leave the hotspot gatherable, and nothing else is offered to it. */
|
|
56
|
+
@Test
|
|
57
|
+
fun `the published hotspot survives the ignore mask`() {
|
|
58
|
+
val merged = mergeScopedNetwork(emptyList(), softAp())
|
|
59
|
+
val mask = SoftApIcePolicy.networkIgnoreMask()
|
|
60
|
+
|
|
61
|
+
val gatherable = merged.filter { SoftApIcePolicy.allowsAdapter(mask, adapterTypeOf(it.type)) }
|
|
62
|
+
|
|
63
|
+
assertThat(gatherable.map { it.name }).containsExactly("wlan1")
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Mirrors libwebrtc's `AdapterTypeFromNetworkType` for the two types this merge produces. */
|
|
67
|
+
private fun adapterTypeOf(type: NetworkChangeDetector.ConnectionType): Int =
|
|
68
|
+
when (type) {
|
|
69
|
+
NetworkChangeDetector.ConnectionType.CONNECTION_WIFI ->
|
|
70
|
+
PeerConnectionFactory.Options.ADAPTER_TYPE_WIFI
|
|
71
|
+
NetworkChangeDetector.ConnectionType.CONNECTION_4G ->
|
|
72
|
+
PeerConnectionFactory.Options.ADAPTER_TYPE_CELLULAR
|
|
73
|
+
else -> PeerConnectionFactory.Options.ADAPTER_TYPE_UNKNOWN
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Without this, ICE never gathers a candidate on the hotspot subnet. */
|
|
77
|
+
@Test
|
|
78
|
+
fun `the scoped interface appears even when the delegate reports nothing`() {
|
|
79
|
+
val merged = mergeScopedNetwork(emptyList(), softAp())
|
|
80
|
+
|
|
81
|
+
assertThat(merged.map { it.name }).containsExactly("wlan1")
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@Test
|
|
85
|
+
fun `a null delegate list is tolerated`() {
|
|
86
|
+
assertThat(mergeScopedNetwork(null, softAp())).hasSize(1)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@Test
|
|
90
|
+
fun `no scoped network leaves the inventory untouched`() {
|
|
91
|
+
val detectedList = listOf(detected("rmnet0"))
|
|
92
|
+
|
|
93
|
+
assertThat(mergeScopedNetwork(detectedList, null).map { it.name }).containsExactly("rmnet0")
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@Test
|
|
97
|
+
fun `a same-named stock wlan0 is replaced by the scoped SoftAP address`() {
|
|
98
|
+
// This phone: stock detector reports wlan0 at 10.15.68.161 (internet Wi-Fi). SoftAP is a
|
|
99
|
+
// different Network that reuses the name wlan0 at 192.168.43.79. Matching on name and
|
|
100
|
+
// keeping the stock entry is how ICE answered with the 10.x host candidate.
|
|
101
|
+
val stockWifi = detected("wlan0")
|
|
102
|
+
val scoped = softAp(name = "wlan0", handle = 99L)
|
|
103
|
+
|
|
104
|
+
val merged = mergeScopedNetwork(listOf(stockWifi), scoped)
|
|
105
|
+
|
|
106
|
+
assertThat(merged).hasSize(1)
|
|
107
|
+
assertThat(merged.single().ipAddresses[0].address).isEqualTo(ipv4(192, 168, 43, 20))
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Handle 0 advertised the right address and then never completed an ICE check (18:56 / 19:01).
|
|
112
|
+
* The real handle is what marks the socket onto the SoftAP; the SDP pin puts the scoped IP
|
|
113
|
+
* back in the answer the glasses see.
|
|
114
|
+
*/
|
|
115
|
+
@Test
|
|
116
|
+
fun `the hotspot is published with its real network handle`() {
|
|
117
|
+
val information = toNetworkInformation(softAp(handle = 99L))
|
|
118
|
+
|
|
119
|
+
assertThat(information).isNotNull
|
|
120
|
+
assertThat(information!!.handle).isEqualTo(99L)
|
|
121
|
+
assertThat(information.type).isEqualTo(NetworkChangeDetector.ConnectionType.CONNECTION_WIFI)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@Test
|
|
125
|
+
fun `the published network carries the hotspot address`() {
|
|
126
|
+
val information = toNetworkInformation(softAp())!!
|
|
127
|
+
|
|
128
|
+
assertThat(information.ipAddresses).hasSize(1)
|
|
129
|
+
assertThat(information.ipAddresses[0].address).isEqualTo(ipv4(192, 168, 43, 20))
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@Test
|
|
133
|
+
fun `an interface with no address is not published`() {
|
|
134
|
+
val addressless = ScopedInterface("wlan1", 42L, emptyList())
|
|
135
|
+
|
|
136
|
+
assertThat(toNetworkInformation(addressless)).isNull()
|
|
137
|
+
assertThat(mergeScopedNetwork(emptyList(), addressless)).isEmpty()
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
@Test
|
|
141
|
+
fun `formatIpv4 renders a dotted-quad`() {
|
|
142
|
+
assertThat(formatIpv4(ipv4(10, 15, 68, 161))).isEqualTo("10.15.68.161")
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
@Test
|
|
146
|
+
fun `multiple addresses are all published`() {
|
|
147
|
+
val multi =
|
|
148
|
+
ScopedInterface("wlan1", 42L, listOf(ipv4(192, 168, 43, 20), ipv4(192, 168, 43, 21)))
|
|
149
|
+
|
|
150
|
+
assertThat(toNetworkInformation(multi)!!.ipAddresses).hasSize(2)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The rejection diagnostics read this, so a handle collision has to be visible in it. */
|
|
154
|
+
@Test
|
|
155
|
+
fun `renderInventory names every entry with its type, handle and addresses`() {
|
|
156
|
+
val rendered = renderInventory(mergeScopedNetwork(emptyList(), softAp()))
|
|
157
|
+
|
|
158
|
+
assertThat(rendered).isEqualTo("wlan1[CONNECTION_WIFI]#42(192.168.43.20)")
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
@Test
|
|
162
|
+
fun `renderInventory tolerates a null list`() {
|
|
163
|
+
assertThat(renderInventory(null)).isEmpty()
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
package com.mentra.acsmeeting.network
|
|
2
|
+
|
|
3
|
+
import com.mentra.acsmeeting.network.ScopedNetworkState.Failure
|
|
4
|
+
import org.assertj.core.api.Assertions.assertThat
|
|
5
|
+
import org.junit.Test
|
|
6
|
+
|
|
7
|
+
class ScopedNetworkErrorTest {
|
|
8
|
+
|
|
9
|
+
@Test
|
|
10
|
+
fun `no failure maps to no error`() {
|
|
11
|
+
assertThat(ScopedNetworkError.from(Failure.NONE, "ssid", 30_000)).isNull()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@Test
|
|
15
|
+
fun `each failure maps to its typed error and stable code`() {
|
|
16
|
+
assertThat(ScopedNetworkError.from(Failure.TIMEOUT, "ap", 30_000))
|
|
17
|
+
.isInstanceOf(ScopedNetworkError.Timeout::class.java)
|
|
18
|
+
.extracting("code")
|
|
19
|
+
.isEqualTo(ScopedNetworkError.CODE_TIMEOUT)
|
|
20
|
+
|
|
21
|
+
assertThat(ScopedNetworkError.from(Failure.UNAVAILABLE, "ap", 30_000))
|
|
22
|
+
.isInstanceOf(ScopedNetworkError.Unavailable::class.java)
|
|
23
|
+
|
|
24
|
+
assertThat(ScopedNetworkError.from(Failure.LOST, "ap", 30_000))
|
|
25
|
+
.isInstanceOf(ScopedNetworkError.Lost::class.java)
|
|
26
|
+
|
|
27
|
+
assertThat(ScopedNetworkError.from(Failure.REQUEST_FAILED, "ap", 30_000))
|
|
28
|
+
.isInstanceOf(ScopedNetworkError.RequestFailed::class.java)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A denied permission must be its own error, not a timeout: otherwise it looks like a flaky
|
|
33
|
+
* hotspot and costs 30 seconds before failing.
|
|
34
|
+
*/
|
|
35
|
+
@Test
|
|
36
|
+
fun `permission denial is distinct from a timeout`() {
|
|
37
|
+
val denied = ScopedNetworkError.from(Failure.PERMISSION_DENIED, "ap", 30_000)
|
|
38
|
+
|
|
39
|
+
assertThat(denied).isInstanceOf(ScopedNetworkError.PermissionDenied::class.java)
|
|
40
|
+
assertThat(denied?.code).isEqualTo(ScopedNetworkError.CODE_PERMISSION_DENIED)
|
|
41
|
+
assertThat(denied?.code).isNotEqualTo(ScopedNetworkError.CODE_TIMEOUT)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@Test
|
|
45
|
+
fun `error codes are stable and unique`() {
|
|
46
|
+
val codes =
|
|
47
|
+
listOf(
|
|
48
|
+
ScopedNetworkError.CODE_PERMISSION_DENIED,
|
|
49
|
+
ScopedNetworkError.CODE_TIMEOUT,
|
|
50
|
+
ScopedNetworkError.CODE_UNAVAILABLE,
|
|
51
|
+
ScopedNetworkError.CODE_LOST,
|
|
52
|
+
ScopedNetworkError.CODE_REQUEST_FAILED,
|
|
53
|
+
ScopedNetworkError.CODE_NO_LOCAL_ADDRESS,
|
|
54
|
+
ScopedNetworkError.CODE_NOT_READY,
|
|
55
|
+
ScopedNetworkError.CODE_WIFI_DISABLED,
|
|
56
|
+
ScopedNetworkError.CODE_VPN_ACTIVE,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
assertThat(codes).doesNotHaveDuplicates()
|
|
60
|
+
assertThat(codes).allMatch { it.startsWith("SOFTAP_") }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@Test
|
|
64
|
+
fun `messages name the ssid so a log line is actionable`() {
|
|
65
|
+
assertThat(ScopedNetworkError.Timeout("MentraLive-1234", 30_000).message)
|
|
66
|
+
.contains("MentraLive-1234")
|
|
67
|
+
.contains("30000")
|
|
68
|
+
assertThat(ScopedNetworkError.Lost("MentraLive-1234").message).contains("MentraLive-1234")
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@Test
|
|
72
|
+
fun `a capturing vpn is named as the cause and tells the user what to change`() {
|
|
73
|
+
val error = ScopedNetworkError.VpnCapturesApp()
|
|
74
|
+
assertThat(error.code).isEqualTo(ScopedNetworkError.CODE_VPN_ACTIVE)
|
|
75
|
+
assertThat(error.message).contains("VPN").contains("split tunneling")
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* "Joined but not usable" has to be its own code. Reported as a join success it becomes an ICE
|
|
80
|
+
* answer with no candidates, which is the symptom several unrelated faults share.
|
|
81
|
+
*/
|
|
82
|
+
@Test
|
|
83
|
+
fun `a network that joined but never became usable names the missing source`() {
|
|
84
|
+
val error = ScopedNetworkError.NotReady("MentraLive-1234", "the kernel interface table")
|
|
85
|
+
|
|
86
|
+
assertThat(error.code).isEqualTo(ScopedNetworkError.CODE_NOT_READY)
|
|
87
|
+
assertThat(error.code).isNotEqualTo(ScopedNetworkError.CODE_NO_LOCAL_ADDRESS)
|
|
88
|
+
assertThat(error.message).contains("MentraLive-1234").contains("the kernel interface table")
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@Test
|
|
92
|
+
fun `the local network permission name is the platform constant`() {
|
|
93
|
+
assertThat(ScopedNetworkError.LOCAL_NETWORK_PERMISSION)
|
|
94
|
+
.isEqualTo("android.permission.ACCESS_LOCAL_NETWORK")
|
|
95
|
+
}
|
|
96
|
+
}
|