@mentra/acs-meeting 3.2.0-dev.223 → 3.2.0-dev.226
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,573 +0,0 @@
|
|
|
1
|
-
package com.mentra.acsmeeting.network
|
|
2
|
-
|
|
3
|
-
import android.content.Context
|
|
4
|
-
import android.content.pm.PackageManager
|
|
5
|
-
import android.net.ConnectivityManager
|
|
6
|
-
import android.net.LinkProperties
|
|
7
|
-
import android.net.Network
|
|
8
|
-
import android.net.NetworkCapabilities
|
|
9
|
-
import android.net.NetworkRequest
|
|
10
|
-
import android.net.wifi.WifiManager
|
|
11
|
-
import android.net.wifi.WifiNetworkSpecifier
|
|
12
|
-
import android.os.Build
|
|
13
|
-
import com.mentra.acsmeeting.trace.SoftApTrace
|
|
14
|
-
import java.net.Inet4Address
|
|
15
|
-
import java.util.concurrent.CountDownLatch
|
|
16
|
-
import java.util.concurrent.TimeUnit
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Joins the glasses' SoftAP as a scoped, internet-less network and keeps the [Network] handle
|
|
20
|
-
* in-process.
|
|
21
|
-
*
|
|
22
|
-
* ## Why this is not reused from the Bluetooth SDK
|
|
23
|
-
*
|
|
24
|
-
* `MentraLocalNetworkModule` in `bluetooth-sdk` already holds a scoped `Network` for the same
|
|
25
|
-
* hotspot, but it only exposes `HttpURLConnection`. A `Network` object cannot cross the JS bridge,
|
|
26
|
-
* and `acs-meeting` has no Gradle edge onto `bluetooth-sdk` — adding one would couple this module to
|
|
27
|
-
* the *published public* SDK and force an internal registry to be exported from it.
|
|
28
|
-
*
|
|
29
|
-
* Owning the join here instead puts the `Network` handle in the same module and lifecycle as the
|
|
30
|
-
* `PeerConnectionFactory`, which is precisely what the libwebrtc interface-agreement problem needs:
|
|
31
|
-
* the ingest source has to bind its sockets to the same network object this class produced. The
|
|
32
|
-
* duplication with the Bluetooth SDK is deliberate.
|
|
33
|
-
*
|
|
34
|
-
* ## Dual-homing
|
|
35
|
-
*
|
|
36
|
-
* The request drops `NET_CAPABILITY_INTERNET` (see [ScopedNetworkRequestSpec]), so Android never
|
|
37
|
-
* promotes the hotspot to the default network and ACS keeps using cellular.
|
|
38
|
-
*/
|
|
39
|
-
class ScopedSoftApNetwork(private val context: Context) {
|
|
40
|
-
|
|
41
|
-
/** Notified on the ConnectivityManager callback thread. */
|
|
42
|
-
interface Listener {
|
|
43
|
-
fun onAvailable(network: Network, localIpv4: String)
|
|
44
|
-
|
|
45
|
-
fun onLost(error: ScopedNetworkError)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
private val lock = Any()
|
|
49
|
-
private val state = ScopedNetworkState()
|
|
50
|
-
private val readiness = ScopedNetworkReadiness()
|
|
51
|
-
|
|
52
|
-
private var callback: ConnectivityManager.NetworkCallback? = null
|
|
53
|
-
private var network: Network? = null
|
|
54
|
-
private var localIpv4: String? = null
|
|
55
|
-
private var spec: ScopedNetworkRequestSpec? = null
|
|
56
|
-
private var listener: Listener? = null
|
|
57
|
-
|
|
58
|
-
/** The joined network, or null. Pass this to anything that must send over the SoftAP link. */
|
|
59
|
-
fun network(): Network? = synchronized(lock) { network }
|
|
60
|
-
|
|
61
|
-
/** This phone's address on the hotspot subnet, e.g. `192.168.43.20`. */
|
|
62
|
-
fun localIpv4(): String? = synchronized(lock) { localIpv4 }
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* The subnet this phone joined, straight from `LinkProperties`.
|
|
66
|
-
*
|
|
67
|
-
* This is the invariant every SoftAP media check is written against: the selected ICE candidate
|
|
68
|
-
* has to be on *this* prefix, not merely on some private range. Reading the prefix length rather
|
|
69
|
-
* than assuming /24 is what keeps the check true when an OEM hands out a different hotspot
|
|
70
|
-
* subnet.
|
|
71
|
-
*/
|
|
72
|
-
fun scopedPrefix(): Ipv4Prefix? {
|
|
73
|
-
val net = network() ?: return null
|
|
74
|
-
val properties = connectivityManager().getLinkProperties(net) ?: return null
|
|
75
|
-
return properties.linkAddresses
|
|
76
|
-
.firstOrNull { it.address is Inet4Address }
|
|
77
|
-
?.let { link -> link.address.hostAddress?.let { Ipv4Prefix(it, link.prefixLength) } }
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
fun isAvailable(): Boolean = synchronized(lock) { state.phase == ScopedNetworkState.Phase.AVAILABLE }
|
|
81
|
-
|
|
82
|
-
/** Station radio on? `WifiNetworkSpecifier` is a no-op while this is false. */
|
|
83
|
-
fun isWifiEnabled(): Boolean {
|
|
84
|
-
val wifi = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as? WifiManager
|
|
85
|
-
return wifi?.isWifiEnabled == true
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Block until the station radio is on, or [timeoutMs] elapses. Used after the system Wi-Fi
|
|
90
|
-
* panel is shown: the join must wait for the user, not abort and remint ACS over a broken
|
|
91
|
-
* default route.
|
|
92
|
-
*/
|
|
93
|
-
fun awaitWifiEnabled(timeoutMs: Long = WIFI_ENABLE_WAIT_MS): Boolean {
|
|
94
|
-
if (isWifiEnabled()) return true
|
|
95
|
-
val deadline = android.os.SystemClock.elapsedRealtime() + timeoutMs
|
|
96
|
-
while (android.os.SystemClock.elapsedRealtime() < deadline) {
|
|
97
|
-
Thread.sleep(WIFI_ENABLE_POLL_MS)
|
|
98
|
-
if (isWifiEnabled()) return true
|
|
99
|
-
}
|
|
100
|
-
return isWifiEnabled()
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* True when a VPN is this app's default network — i.e. our UID falls inside its captured
|
|
105
|
-
* ranges. A VPN that excludes us (split tunneling) or is simply installed does not count: the
|
|
106
|
-
* default network is then cellular/Wi-Fi and the hotspot path works.
|
|
107
|
-
*/
|
|
108
|
-
fun isVpnCapturingApp(): Boolean {
|
|
109
|
-
val manager = connectivityManager()
|
|
110
|
-
val active = manager.activeNetwork ?: return false
|
|
111
|
-
val capabilities = manager.getNetworkCapabilities(active) ?: return false
|
|
112
|
-
return capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/** Verdict of [probeGateway]: [detail] is one human-readable line for the UI and the trace. */
|
|
116
|
-
data class GatewayProbe(val reachable: Boolean, val detail: String)
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* The hotspot's own address on the joined network — the DHCP server, since the glasses
|
|
120
|
-
* deliberately advertise no default route. Falls back to `.1` of the phone's /24.
|
|
121
|
-
*/
|
|
122
|
-
fun gatewayIpv4(): String? {
|
|
123
|
-
val net = network() ?: return null
|
|
124
|
-
val properties = connectivityManager().getLinkProperties(net)
|
|
125
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
126
|
-
properties?.dhcpServerAddress?.hostAddress?.let { return it }
|
|
127
|
-
}
|
|
128
|
-
val local = localIpv4() ?: return null
|
|
129
|
-
val prefix = local.substringBeforeLast('.', "")
|
|
130
|
-
return if (prefix.isEmpty()) null else "$prefix.1"
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Prove the phone can reach the glasses over the network it just joined.
|
|
135
|
-
*
|
|
136
|
-
* TCP, through [Network.getSocketFactory], so the kernel routes via the hotspot's table rather
|
|
137
|
-
* than the cellular default that unbound sockets would take. A *refused* connection is a pass:
|
|
138
|
-
* the SYN arrived and the RST came back, which is the reachability question. Only a timeout or
|
|
139
|
-
* an unreachable-network error is a fail.
|
|
140
|
-
*/
|
|
141
|
-
fun probeGateway(timeoutMs: Int = GATEWAY_PROBE_TIMEOUT_MS): GatewayProbe {
|
|
142
|
-
val net = network() ?: return GatewayProbe(false, "no scoped network")
|
|
143
|
-
val gateway = gatewayIpv4() ?: return GatewayProbe(false, "no gateway address on the scoped network")
|
|
144
|
-
var lastFailure = "no ports tried"
|
|
145
|
-
for (port in GATEWAY_PROBE_PORTS) {
|
|
146
|
-
val startedAt = android.os.SystemClock.elapsedRealtime()
|
|
147
|
-
val socket =
|
|
148
|
-
try {
|
|
149
|
-
net.socketFactory.createSocket()
|
|
150
|
-
} catch (error: java.io.IOException) {
|
|
151
|
-
// netd answers EPERM when a VPN owns our UID and forbids binding elsewhere.
|
|
152
|
-
val message = error.message ?: error.javaClass.simpleName
|
|
153
|
-
val detail =
|
|
154
|
-
if (message.contains("EPERM")) {
|
|
155
|
-
"cannot bind to the hotspot network ($message): a VPN is capturing this app"
|
|
156
|
-
} else {
|
|
157
|
-
"cannot bind to the hotspot network: $message"
|
|
158
|
-
}
|
|
159
|
-
SoftApTrace.failure("gateway_probe", "gateway" to gateway, "result" to detail)
|
|
160
|
-
return GatewayProbe(false, detail)
|
|
161
|
-
}
|
|
162
|
-
try {
|
|
163
|
-
socket.connect(java.net.InetSocketAddress(gateway, port), timeoutMs)
|
|
164
|
-
val elapsed = android.os.SystemClock.elapsedRealtime() - startedAt
|
|
165
|
-
SoftApTrace.stage("gateway_probe", "gateway" to gateway, "port" to port, "result" to "connected", "ms" to elapsed)
|
|
166
|
-
return GatewayProbe(true, "tcp $gateway:$port connected in ${elapsed}ms")
|
|
167
|
-
} catch (error: java.net.ConnectException) {
|
|
168
|
-
val elapsed = android.os.SystemClock.elapsedRealtime() - startedAt
|
|
169
|
-
val message = error.message ?: ""
|
|
170
|
-
if (message.contains("refused", ignoreCase = true)) {
|
|
171
|
-
SoftApTrace.stage("gateway_probe", "gateway" to gateway, "port" to port, "result" to "refused", "ms" to elapsed)
|
|
172
|
-
return GatewayProbe(true, "tcp $gateway:$port refused (host answered) in ${elapsed}ms")
|
|
173
|
-
}
|
|
174
|
-
lastFailure = "tcp $gateway:$port $message"
|
|
175
|
-
} catch (error: java.net.SocketTimeoutException) {
|
|
176
|
-
lastFailure = "tcp $gateway:$port timed out after ${timeoutMs}ms"
|
|
177
|
-
} catch (error: java.io.IOException) {
|
|
178
|
-
lastFailure = "tcp $gateway:$port ${error.message ?: error.javaClass.simpleName}"
|
|
179
|
-
} finally {
|
|
180
|
-
runCatching { socket.close() }
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
SoftApTrace.failure("gateway_probe", "gateway" to gateway, "result" to lastFailure)
|
|
184
|
-
return GatewayProbe(false, lastFailure)
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Whether the local-network permission is granted. Below the SDK level that enforces it, access
|
|
189
|
-
* is implicit and this reports true.
|
|
190
|
-
*/
|
|
191
|
-
fun hasLocalNetworkPermission(): Boolean =
|
|
192
|
-
hasLocalNetworkPermission(Build.VERSION.SDK_INT, context.applicationInfo.targetSdkVersion) {
|
|
193
|
-
context.checkSelfPermission(ScopedNetworkError.LOCAL_NETWORK_PERMISSION) ==
|
|
194
|
-
PackageManager.PERMISSION_GRANTED
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Join [ssid] and block until it is usable or the request fails.
|
|
199
|
-
*
|
|
200
|
-
* @throws ScopedNetworkError on permission denial, timeout, or an unavailable network
|
|
201
|
-
*/
|
|
202
|
-
@Throws(ScopedNetworkError::class)
|
|
203
|
-
fun join(ssid: String, passphrase: String, listener: Listener? = null): Network {
|
|
204
|
-
if (!hasLocalNetworkPermission()) {
|
|
205
|
-
SoftApTrace.failure("scoped_join_permission_denied", "ssid" to ssid)
|
|
206
|
-
throw ScopedNetworkError.PermissionDenied(ScopedNetworkError.LOCAL_NETWORK_PERMISSION)
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// A disabled radio makes WifiNetworkSpecifier fail as Unavailable in under 10ms, which is
|
|
210
|
-
// indistinguishable from a hotspot that never came up. Report the real cause instead.
|
|
211
|
-
if (!isWifiEnabled()) {
|
|
212
|
-
SoftApTrace.failure("scoped_join_wifi_disabled", "ssid" to ssid)
|
|
213
|
-
throw ScopedNetworkError.WifiDisabled()
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// The specifier join would succeed, but the glasses could never complete a TCP handshake
|
|
217
|
-
// to our listener and we could not bind to the hotspot. Fail here, where the message can
|
|
218
|
-
// say why, instead of ten seconds into the camera step.
|
|
219
|
-
if (isVpnCapturingApp()) {
|
|
220
|
-
SoftApTrace.failure("scoped_join_vpn_captures_app", "ssid" to ssid)
|
|
221
|
-
throw ScopedNetworkError.VpnCapturesApp()
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
release()
|
|
225
|
-
|
|
226
|
-
val requestSpec = ScopedNetworkRequestSpec.forSoftAp(ssid, passphrase)
|
|
227
|
-
val manager = connectivityManager()
|
|
228
|
-
val ready = CountDownLatch(1)
|
|
229
|
-
|
|
230
|
-
val generation: Int
|
|
231
|
-
synchronized(lock) {
|
|
232
|
-
generation = state.startRequest()
|
|
233
|
-
spec = requestSpec
|
|
234
|
-
this.listener = listener
|
|
235
|
-
readiness.reset()
|
|
236
|
-
}
|
|
237
|
-
SoftApTrace.stage(
|
|
238
|
-
"scoped_join_requested",
|
|
239
|
-
"ssid" to ssid,
|
|
240
|
-
"timeoutMs" to requestSpec.timeoutMs,
|
|
241
|
-
"avoidsInternetCapability" to requestSpec.avoidsInternetCapability,
|
|
242
|
-
)
|
|
243
|
-
val startedAtMs = System.currentTimeMillis()
|
|
244
|
-
|
|
245
|
-
val networkCallback =
|
|
246
|
-
object : ConnectivityManager.NetworkCallback() {
|
|
247
|
-
override fun onAvailable(available: Network) {
|
|
248
|
-
val properties = manager.getLinkProperties(available)
|
|
249
|
-
val resolvedIpv4 = firstIpv4(properties)
|
|
250
|
-
synchronized(lock) {
|
|
251
|
-
if (!state.onAvailable(generation)) return
|
|
252
|
-
network = available
|
|
253
|
-
localIpv4 = resolvedIpv4
|
|
254
|
-
readiness.onAvailable()
|
|
255
|
-
readiness.seedLinkProperties(properties?.interfaceName, resolvedIpv4)
|
|
256
|
-
}
|
|
257
|
-
SoftApTrace.stage(
|
|
258
|
-
"scoped_network_available",
|
|
259
|
-
"ssid" to ssid,
|
|
260
|
-
"localIpv4" to resolvedIpv4,
|
|
261
|
-
"joinMs" to (System.currentTimeMillis() - startedAtMs),
|
|
262
|
-
"defaultNetworkIsCellular" to defaultNetworkIsCellular(manager),
|
|
263
|
-
)
|
|
264
|
-
// Does the address we were just handed exist in the kernel's interface table,
|
|
265
|
-
// and under the name LinkProperties claims? ICE gathers from that table, so a
|
|
266
|
-
// mismatch here is the difference between a hotspot candidate and none. The
|
|
267
|
-
// address can also land in the table *after* onAvailable, which is why this is
|
|
268
|
-
// only a snapshot for the trace and readiness is decided by polling below.
|
|
269
|
-
val table = NetworkFacts.snapshot()
|
|
270
|
-
SoftApTrace.stage(
|
|
271
|
-
"scoped_network_ifaddrs",
|
|
272
|
-
"localIpv4" to resolvedIpv4,
|
|
273
|
-
"claimed" to (properties?.interfaceName ?: "none"),
|
|
274
|
-
"owner" to (resolvedIpv4?.let { NetworkFacts.ownerOf(it, table) } ?: "ABSENT"),
|
|
275
|
-
"gatherable" to NetworkFacts.render(NetworkFacts.gatherable(table)),
|
|
276
|
-
)
|
|
277
|
-
if (readiness.callbacksSatisfied()) ready.countDown()
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* The callback Android says to read properties from. It also arrives again when
|
|
282
|
-
* DHCP completes, which is the case `onAvailable` alone missed.
|
|
283
|
-
*/
|
|
284
|
-
override fun onLinkPropertiesChanged(
|
|
285
|
-
changed: Network,
|
|
286
|
-
properties: LinkProperties,
|
|
287
|
-
) {
|
|
288
|
-
val address = firstIpv4(properties)
|
|
289
|
-
synchronized(lock) {
|
|
290
|
-
if (!state.accepts(generation)) return
|
|
291
|
-
// Sticky, unlike the readiness observation: the WHIP listener is bound to
|
|
292
|
-
// this address and mid-call disappearance is `onLost`'s business, so a
|
|
293
|
-
// transient property update must not blank the ingest URL.
|
|
294
|
-
if (address != null) localIpv4 = address
|
|
295
|
-
readiness.onLinkProperties(properties.interfaceName, address)
|
|
296
|
-
}
|
|
297
|
-
SoftApTrace.stage(
|
|
298
|
-
"scoped_link_properties",
|
|
299
|
-
"interface" to (properties.interfaceName ?: "none"),
|
|
300
|
-
"localIpv4" to (address ?: "none"),
|
|
301
|
-
)
|
|
302
|
-
if (readiness.callbacksSatisfied()) ready.countDown()
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
override fun onCapabilitiesChanged(
|
|
306
|
-
changed: Network,
|
|
307
|
-
capabilities: NetworkCapabilities,
|
|
308
|
-
) {
|
|
309
|
-
synchronized(lock) {
|
|
310
|
-
if (!state.accepts(generation)) return
|
|
311
|
-
readiness.onCapabilities()
|
|
312
|
-
}
|
|
313
|
-
if (readiness.callbacksSatisfied()) ready.countDown()
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
override fun onUnavailable() {
|
|
317
|
-
synchronized(lock) { if (!state.onUnavailable(generation)) return }
|
|
318
|
-
SoftApTrace.failure("scoped_network_unavailable", "ssid" to ssid)
|
|
319
|
-
ready.countDown()
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
override fun onLost(lost: Network) {
|
|
323
|
-
val notify: Listener?
|
|
324
|
-
synchronized(lock) {
|
|
325
|
-
if (!state.onLost(generation)) return
|
|
326
|
-
network = null
|
|
327
|
-
localIpv4 = null
|
|
328
|
-
notify = this@ScopedSoftApNetwork.listener
|
|
329
|
-
}
|
|
330
|
-
SoftApTrace.failure("scoped_network_lost", "ssid" to ssid)
|
|
331
|
-
ready.countDown()
|
|
332
|
-
notify?.onLost(ScopedNetworkError.Lost(ssid))
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
synchronized(lock) { callback = networkCallback }
|
|
337
|
-
|
|
338
|
-
try {
|
|
339
|
-
manager.requestNetwork(request(requestSpec), networkCallback, requestSpec.timeoutMs)
|
|
340
|
-
} catch (error: SecurityException) {
|
|
341
|
-
synchronized(lock) { state.onRequestFailed(generation, permissionDenied = true) }
|
|
342
|
-
clearCallback(networkCallback)
|
|
343
|
-
SoftApTrace.failure("scoped_join_security_exception", "ssid" to ssid)
|
|
344
|
-
throw ScopedNetworkError.PermissionDenied(ScopedNetworkError.LOCAL_NETWORK_PERMISSION)
|
|
345
|
-
} catch (error: Exception) {
|
|
346
|
-
synchronized(lock) { state.onRequestFailed(generation, permissionDenied = false) }
|
|
347
|
-
clearCallback(networkCallback)
|
|
348
|
-
SoftApTrace.failure("scoped_join_request_failed", "ssid" to ssid)
|
|
349
|
-
throw ScopedNetworkError.RequestFailed(error.message ?: "unknown")
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// requestNetwork's own timeout fires onUnavailable, but a slightly longer await guards
|
|
353
|
-
// against never being called back at all.
|
|
354
|
-
val awaited =
|
|
355
|
-
ready.await(requestSpec.timeoutMs.toLong() + AWAIT_GRACE_MS, TimeUnit.MILLISECONDS)
|
|
356
|
-
if (!awaited) synchronized(lock) { state.onTimeout(generation) }
|
|
357
|
-
|
|
358
|
-
val joinedNetwork =
|
|
359
|
-
synchronized(lock) {
|
|
360
|
-
network.takeIf { state.phase == ScopedNetworkState.Phase.AVAILABLE }
|
|
361
|
-
}
|
|
362
|
-
if (joinedNetwork != null) {
|
|
363
|
-
val verdict = awaitReadiness(ssid)
|
|
364
|
-
val notify = synchronized(lock) { listener }
|
|
365
|
-
notify?.onAvailable(joinedNetwork, verdict.address)
|
|
366
|
-
probeUnmarkedUdpSend(verdict.address)
|
|
367
|
-
return joinedNetwork
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
throw synchronized(lock) {
|
|
371
|
-
val failure =
|
|
372
|
-
ScopedNetworkError.from(state.failure, ssid, requestSpec.timeoutMs)
|
|
373
|
-
?: ScopedNetworkError.Timeout(ssid, requestSpec.timeoutMs)
|
|
374
|
-
releaseLocked()
|
|
375
|
-
failure
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
/**
|
|
380
|
-
* Block until the joined network is ready for ICE, or fail with the source that never reported.
|
|
381
|
-
*
|
|
382
|
-
* The kernel interface table is the one input with no callback, so it is polled: the hotspot
|
|
383
|
-
* address is regularly absent when `onAvailable` returns and appears a moment later. Starting
|
|
384
|
-
* ingest before it exists produced an answer with zero candidates, which looked identical to a
|
|
385
|
-
* binding bug and was misdiagnosed as one.
|
|
386
|
-
*/
|
|
387
|
-
@Throws(ScopedNetworkError::class)
|
|
388
|
-
private fun awaitReadiness(ssid: String): ScopedNetworkReadiness.Verdict.Ready {
|
|
389
|
-
val deadline = android.os.SystemClock.elapsedRealtime() + READINESS_WAIT_MS
|
|
390
|
-
var verdict: ScopedNetworkReadiness.Verdict
|
|
391
|
-
while (true) {
|
|
392
|
-
val address = readiness.observations.linkAddress
|
|
393
|
-
readiness.onInterfaceTable(address?.let { NetworkFacts.ownerOf(it) })
|
|
394
|
-
verdict = readiness.verdict()
|
|
395
|
-
if (verdict is ScopedNetworkReadiness.Verdict.Ready) break
|
|
396
|
-
if (android.os.SystemClock.elapsedRealtime() >= deadline) break
|
|
397
|
-
Thread.sleep(READINESS_POLL_MS)
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
val observed = readiness.observations
|
|
401
|
-
if (verdict is ScopedNetworkReadiness.Verdict.Ready) {
|
|
402
|
-
SoftApTrace.stage(
|
|
403
|
-
"scoped_network_ready",
|
|
404
|
-
"localIpv4" to verdict.address,
|
|
405
|
-
"claimed" to (verdict.interfaceName ?: "none"),
|
|
406
|
-
"owner" to verdict.owner,
|
|
407
|
-
"agrees" to (verdict.interfaceName == null || verdict.interfaceName == verdict.owner),
|
|
408
|
-
"capabilitiesSeen" to observed.capabilities,
|
|
409
|
-
)
|
|
410
|
-
return verdict
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
val waiting = verdict as ScopedNetworkReadiness.Verdict.Waiting
|
|
414
|
-
SoftApTrace.failure(
|
|
415
|
-
"scoped_network_not_ready",
|
|
416
|
-
"ssid" to ssid,
|
|
417
|
-
"missing" to waiting.missing,
|
|
418
|
-
"localIpv4" to (observed.linkAddress ?: "none"),
|
|
419
|
-
"claimed" to (observed.linkInterface ?: "none"),
|
|
420
|
-
"capabilitiesSeen" to observed.capabilities,
|
|
421
|
-
"gatherable" to NetworkFacts.render(NetworkFacts.gatherable()),
|
|
422
|
-
)
|
|
423
|
-
release()
|
|
424
|
-
throw ScopedNetworkError.NotReady(ssid, waiting.missing)
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
/**
|
|
428
|
-
* Whether an unmarked UDP socket on the hotspot address can even be created and written to.
|
|
429
|
-
*
|
|
430
|
-
* Diagnostic only, and deliberately not a gate. A successful `send` means the OS accepted the
|
|
431
|
-
* datagram; it says nothing about whether the glasses received it or can reply, so it cannot
|
|
432
|
-
* establish that unmarked UDP works. What it *can* do is separate "bind refused outright" from
|
|
433
|
-
* "bound fine, nothing came back", which is the FAILED_BINDING hypothesis. The real answer to
|
|
434
|
-
* whether this path carries traffic is the ICE connectivity-check result.
|
|
435
|
-
*/
|
|
436
|
-
private fun probeUnmarkedUdpSend(address: String) {
|
|
437
|
-
val gateway = gatewayIpv4()
|
|
438
|
-
val result =
|
|
439
|
-
runCatching {
|
|
440
|
-
java.net.DatagramSocket(0, java.net.InetAddress.getByName(address)).use { socket ->
|
|
441
|
-
val target = gateway ?: return@runCatching "bound to $address; no gateway to send to"
|
|
442
|
-
socket.send(
|
|
443
|
-
java.net.DatagramPacket(
|
|
444
|
-
ByteArray(1),
|
|
445
|
-
1,
|
|
446
|
-
java.net.InetAddress.getByName(target),
|
|
447
|
-
UDP_PROBE_PORT,
|
|
448
|
-
),
|
|
449
|
-
)
|
|
450
|
-
"bound to ${socket.localAddress?.hostAddress}:${socket.localPort}, send accepted"
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
SoftApTrace.stage(
|
|
454
|
-
"unmarked_udp_send",
|
|
455
|
-
"address" to address,
|
|
456
|
-
"gateway" to (gateway ?: "none"),
|
|
457
|
-
"accepted" to result.isSuccess,
|
|
458
|
-
"detail" to
|
|
459
|
-
(result.getOrNull() ?: result.exceptionOrNull()?.let { "${it.javaClass.simpleName}: ${it.message}" }),
|
|
460
|
-
"proves" to "the OS accepted a datagram; not that the glasses received one",
|
|
461
|
-
)
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
/** Unregister the callback and drop the network. Safe to call twice. */
|
|
465
|
-
fun release() {
|
|
466
|
-
synchronized(lock) { releaseLocked() }
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
private fun releaseLocked() {
|
|
470
|
-
val active = callback
|
|
471
|
-
if (active != null) {
|
|
472
|
-
runCatching { connectivityManager().unregisterNetworkCallback(active) }
|
|
473
|
-
SoftApTrace.stage("scoped_network_released")
|
|
474
|
-
}
|
|
475
|
-
callback = null
|
|
476
|
-
network = null
|
|
477
|
-
localIpv4 = null
|
|
478
|
-
spec = null
|
|
479
|
-
listener = null
|
|
480
|
-
state.release()
|
|
481
|
-
state.reset()
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
private fun clearCallback(expected: ConnectivityManager.NetworkCallback) {
|
|
485
|
-
synchronized(lock) {
|
|
486
|
-
if (callback !== expected) return
|
|
487
|
-
runCatching { connectivityManager().unregisterNetworkCallback(expected) }
|
|
488
|
-
callback = null
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
private fun connectivityManager(): ConnectivityManager =
|
|
493
|
-
requireNotNull(context.getSystemService(ConnectivityManager::class.java)) {
|
|
494
|
-
"ConnectivityManager is unavailable"
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
/** Translate the pure spec into the framework request. */
|
|
500
|
-
private fun request(spec: ScopedNetworkRequestSpec): NetworkRequest {
|
|
501
|
-
val specifier =
|
|
502
|
-
WifiNetworkSpecifier.Builder()
|
|
503
|
-
.setSsid(spec.ssid)
|
|
504
|
-
.apply { if (spec.hasPassphrase) setWpa2Passphrase(spec.passphrase) }
|
|
505
|
-
.build()
|
|
506
|
-
|
|
507
|
-
val builder = NetworkRequest.Builder()
|
|
508
|
-
spec.transportTypes.forEach { builder.addTransportType(it) }
|
|
509
|
-
spec.removedCapabilities.forEach { builder.removeCapability(it) }
|
|
510
|
-
return builder.setNetworkSpecifier(specifier).build()
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
private fun firstIpv4(properties: LinkProperties?): String? =
|
|
514
|
-
properties
|
|
515
|
-
?.linkAddresses
|
|
516
|
-
?.firstOrNull { it.address is Inet4Address }
|
|
517
|
-
?.address
|
|
518
|
-
?.hostAddress
|
|
519
|
-
|
|
520
|
-
/** Dual-homing check: the default route must still be cellular after the scoped join. */
|
|
521
|
-
private fun defaultNetworkIsCellular(manager: ConnectivityManager): Boolean {
|
|
522
|
-
val active = manager.activeNetwork ?: return false
|
|
523
|
-
val capabilities = manager.getNetworkCapabilities(active) ?: return false
|
|
524
|
-
return capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
companion object {
|
|
528
|
-
/** Android 17. `ACCESS_LOCAL_NETWORK` is enforced for apps targeting SDK 37+. */
|
|
529
|
-
const val LOCAL_NETWORK_ENFORCED_SDK = 37
|
|
530
|
-
|
|
531
|
-
/** Legacy targets retain implicit LAN access, even when the runtime grant reports denied. */
|
|
532
|
-
internal fun hasLocalNetworkPermission(
|
|
533
|
-
sdkInt: Int,
|
|
534
|
-
targetSdkInt: Int,
|
|
535
|
-
permissionGranted: () -> Boolean,
|
|
536
|
-
): Boolean =
|
|
537
|
-
sdkInt < LOCAL_NETWORK_ENFORCED_SDK ||
|
|
538
|
-
targetSdkInt < LOCAL_NETWORK_ENFORCED_SDK ||
|
|
539
|
-
permissionGranted()
|
|
540
|
-
|
|
541
|
-
private const val AWAIT_GRACE_MS = 2_000L
|
|
542
|
-
|
|
543
|
-
/** How long we hold the scoped-join step open for the user to flip the Wi-Fi toggle. */
|
|
544
|
-
const val WIFI_ENABLE_WAIT_MS = 90_000L
|
|
545
|
-
const val WIFI_ENABLE_POLL_MS = 300L
|
|
546
|
-
/** Radio + scan need a beat after `isWifiEnabled` flips before specifier can see the AP. */
|
|
547
|
-
const val WIFI_ENABLE_SETTLE_MS = 1_500L
|
|
548
|
-
|
|
549
|
-
/**
|
|
550
|
-
* After `WifiNetworkSpecifier` returns Unavailable while leaving another AP (office Wi-Fi
|
|
551
|
-
* or a phone hotspot), Samsung has already dropped that network. A second specifier from
|
|
552
|
-
* an idle STA is the join that succeeds; this is the gap before we issue it.
|
|
553
|
-
*/
|
|
554
|
-
const val UNAVAILABLE_RETRY_SETTLE_MS = 2_000L
|
|
555
|
-
|
|
556
|
-
/**
|
|
557
|
-
* dnsmasq (DHCP/DNS) on the hotspot, then the glasses' local HTTP servers. Any answer —
|
|
558
|
-
* accept or refuse — proves the path; only silence fails it.
|
|
559
|
-
*/
|
|
560
|
-
private val GATEWAY_PROBE_PORTS = intArrayOf(53, 8089, 80)
|
|
561
|
-
private const val GATEWAY_PROBE_TIMEOUT_MS = 2_000
|
|
562
|
-
|
|
563
|
-
/**
|
|
564
|
-
* How long the address is given to appear in the kernel table after the callbacks agree.
|
|
565
|
-
* Generous because failing here aborts the call, and the observed lag is sub-second.
|
|
566
|
-
*/
|
|
567
|
-
private const val READINESS_WAIT_MS = 5_000L
|
|
568
|
-
private const val READINESS_POLL_MS = 150L
|
|
569
|
-
|
|
570
|
-
/** dnsmasq on the hotspot. Nothing is expected back; only the send is being observed. */
|
|
571
|
-
private const val UDP_PROBE_PORT = 53
|
|
572
|
-
}
|
|
573
|
-
}
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
package com.mentra.acsmeeting.source
|
|
2
|
-
|
|
3
|
-
import java.nio.ByteBuffer
|
|
4
|
-
|
|
5
|
-
/** 5x7 glyphs for the synthetic stamp. No Android canvas, so this is the font. */
|
|
6
|
-
internal object BitmapFont {
|
|
7
|
-
const val GLYPH_W = 5
|
|
8
|
-
const val GLYPH_H = 7
|
|
9
|
-
|
|
10
|
-
fun draw(
|
|
11
|
-
dest: ByteBuffer,
|
|
12
|
-
frameWidth: Int,
|
|
13
|
-
frameHeight: Int,
|
|
14
|
-
text: String,
|
|
15
|
-
originX: Int,
|
|
16
|
-
originY: Int,
|
|
17
|
-
scale: Int,
|
|
18
|
-
on: Byte,
|
|
19
|
-
) {
|
|
20
|
-
var x = originX
|
|
21
|
-
for (ch in text) {
|
|
22
|
-
val glyph = glyph(ch) ?: continue
|
|
23
|
-
blit(dest, frameWidth, frameHeight, glyph, x, originY, scale, on)
|
|
24
|
-
x += (GLYPH_W + 1) * scale
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
fun textWidth(length: Int, scale: Int): Int =
|
|
29
|
-
if (length <= 0) 0 else length * (GLYPH_W + 1) * scale - scale
|
|
30
|
-
|
|
31
|
-
fun textHeight(scale: Int): Int = GLYPH_H * scale
|
|
32
|
-
|
|
33
|
-
private fun blit(
|
|
34
|
-
dest: ByteBuffer,
|
|
35
|
-
frameWidth: Int,
|
|
36
|
-
frameHeight: Int,
|
|
37
|
-
glyph: IntArray,
|
|
38
|
-
originX: Int,
|
|
39
|
-
originY: Int,
|
|
40
|
-
scale: Int,
|
|
41
|
-
on: Byte,
|
|
42
|
-
) {
|
|
43
|
-
for (row in 0 until GLYPH_H) {
|
|
44
|
-
val bits = glyph[row]
|
|
45
|
-
for (col in 0 until GLYPH_W) {
|
|
46
|
-
if ((bits shr (GLYPH_W - 1 - col)) and 1 == 0) continue
|
|
47
|
-
val x0 = originX + col * scale
|
|
48
|
-
val y0 = originY + row * scale
|
|
49
|
-
val xEnd = (x0 + scale).coerceAtMost(frameWidth)
|
|
50
|
-
val yEnd = (y0 + scale).coerceAtMost(frameHeight)
|
|
51
|
-
val xStart = x0.coerceAtLeast(0)
|
|
52
|
-
val yStart = y0.coerceAtLeast(0)
|
|
53
|
-
if (xStart >= xEnd || yStart >= yEnd) continue
|
|
54
|
-
for (y in yStart until yEnd) {
|
|
55
|
-
val rowOff = y * frameWidth
|
|
56
|
-
for (x in xStart until xEnd) dest.put(rowOff + x, on)
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
private fun glyph(ch: Char): IntArray? = when (ch) {
|
|
63
|
-
'0' -> G0
|
|
64
|
-
'1' -> G1
|
|
65
|
-
'2' -> G2
|
|
66
|
-
'3' -> G3
|
|
67
|
-
'4' -> G4
|
|
68
|
-
'5' -> G5
|
|
69
|
-
'6' -> G6
|
|
70
|
-
'7' -> G7
|
|
71
|
-
'8' -> G8
|
|
72
|
-
'9' -> G9
|
|
73
|
-
'F' -> GF
|
|
74
|
-
'P' -> GP
|
|
75
|
-
'S' -> GS
|
|
76
|
-
'X' -> GX
|
|
77
|
-
'x' -> GX
|
|
78
|
-
'.' -> GDOT
|
|
79
|
-
' ' -> GSPACE
|
|
80
|
-
else -> null
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
private val G0 = intArrayOf(0b01110, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110)
|
|
84
|
-
private val G1 = intArrayOf(0b00100, 0b01100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110)
|
|
85
|
-
private val G2 = intArrayOf(0b01110, 0b10001, 0b00001, 0b00010, 0b00100, 0b01000, 0b11111)
|
|
86
|
-
private val G3 = intArrayOf(0b01110, 0b10001, 0b00001, 0b00110, 0b00001, 0b10001, 0b01110)
|
|
87
|
-
private val G4 = intArrayOf(0b00010, 0b00110, 0b01010, 0b10010, 0b11111, 0b00010, 0b00010)
|
|
88
|
-
private val G5 = intArrayOf(0b11111, 0b10000, 0b11110, 0b00001, 0b00001, 0b10001, 0b01110)
|
|
89
|
-
private val G6 = intArrayOf(0b01110, 0b10000, 0b11110, 0b10001, 0b10001, 0b10001, 0b01110)
|
|
90
|
-
private val G7 = intArrayOf(0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b01000, 0b01000)
|
|
91
|
-
private val G8 = intArrayOf(0b01110, 0b10001, 0b10001, 0b01110, 0b10001, 0b10001, 0b01110)
|
|
92
|
-
private val G9 = intArrayOf(0b01110, 0b10001, 0b10001, 0b01111, 0b00001, 0b00001, 0b01110)
|
|
93
|
-
private val GF = intArrayOf(0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b10000)
|
|
94
|
-
private val GP = intArrayOf(0b11110, 0b10001, 0b10001, 0b11110, 0b10000, 0b10000, 0b10000)
|
|
95
|
-
private val GS = intArrayOf(0b01111, 0b10000, 0b10000, 0b01110, 0b00001, 0b00001, 0b11110)
|
|
96
|
-
private val GX = intArrayOf(0b10001, 0b10001, 0b01010, 0b00100, 0b01010, 0b10001, 0b10001)
|
|
97
|
-
private val GDOT = intArrayOf(0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00100, 0b00100)
|
|
98
|
-
private val GSPACE = intArrayOf(0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000)
|
|
99
|
-
}
|