@mentra/bluetooth-sdk 0.1.21-beta.3 → 3.1.0-beta.100
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 +67 -25
- package/android/build.gradle +7 -4
- package/android/lc3Lib/build.gradle +18 -4
- package/android/silero/build.gradle +9 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +16 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +35 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +216 -23
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +2 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +7 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +11 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +127 -8
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraLocalNetworkModule.kt +93 -12
- package/android/src/main/java/com/mentra/bluetoothsdk/OtaManifest.kt +5 -7
- package/android/src/main/java/com/mentra/bluetoothsdk/ReleaseChangelog.kt +61 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/ScopedNetworkReadiness.kt +17 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/audio/AudioModels.kt +15 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +24 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/net/LocalIpv4.kt +57 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/otaserver/LocalOtaServer.kt +304 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/otaserver/MentraOtaServerModule.kt +120 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/MentraPhotoReceiverModule.kt +2 -49
- package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +61 -30
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/BesOtaHeartbeatGuard.kt +49 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/BesOtaProgressMapper.kt +27 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +1393 -284
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingAdvertisement.kt +54 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +8 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +17 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/types/DeviceModels.kt +12 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/BleJsonCompact.java +74 -79
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/K900ProtocolUtils.java +3 -3
- package/android/src/test/java/com/mentra/bluetoothsdk/OtaStartResponsePolicyTest.kt +30 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/ReleaseChangelogTest.kt +13 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/ScopedNetworkReadinessTest.kt +39 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/otaserver/LocalOtaServerTest.kt +82 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/services/ForegroundServiceTypeTest.kt +69 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/BesOtaHeartbeatGuardTest.kt +73 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/BesOtaProgressMapperTest.kt +67 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattCharacteristicsTest.kt +39 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingAdvertisementParserTest.kt +133 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/BleJsonCompactTest.java +148 -7
- package/build/BluetoothSdk.types.d.ts +84 -2
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_internal.d.ts +2 -0
- package/build/_internal.d.ts.map +1 -1
- package/build/_internal.js +2 -0
- package/build/_internal.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +4 -1
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +1 -1
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/MentraLocalNetworkModule.d.ts +1 -0
- package/build/_private/MentraLocalNetworkModule.d.ts.map +1 -1
- package/build/_private/MentraLocalNetworkModule.js.map +1 -1
- package/build/_private/sdkOtaManifest.d.ts +3 -12
- package/build/_private/sdkOtaManifest.d.ts.map +1 -1
- package/build/_private/sdkOtaManifest.js +5 -17
- package/build/_private/sdkOtaManifest.js.map +1 -1
- package/build/changelogs.d.ts +7 -0
- package/build/changelogs.d.ts.map +1 -0
- package/build/changelogs.js +44 -0
- package/build/changelogs.js.map +1 -0
- package/build/generated/changelogCatalog.d.ts +6 -0
- package/build/generated/changelogCatalog.d.ts.map +1 -0
- package/build/generated/changelogCatalog.js +8 -0
- package/build/generated/changelogCatalog.js.map +1 -0
- package/build/generated/releaseMetadata.d.ts +12 -0
- package/build/generated/releaseMetadata.d.ts.map +1 -0
- package/build/generated/releaseMetadata.js +10 -0
- package/build/generated/releaseMetadata.js.map +1 -0
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +24 -2
- package/build/index.js.map +1 -1
- package/build/ota-server/MentraOtaServer.types.d.ts +14 -0
- package/build/ota-server/MentraOtaServer.types.d.ts.map +1 -0
- package/build/ota-server/MentraOtaServer.types.js +2 -0
- package/build/ota-server/MentraOtaServer.types.js.map +1 -0
- package/build/ota-server/MentraOtaServerModule.d.ts +21 -0
- package/build/ota-server/MentraOtaServerModule.d.ts.map +1 -0
- package/build/ota-server/MentraOtaServerModule.js +3 -0
- package/build/ota-server/MentraOtaServerModule.js.map +1 -0
- package/build/ota-server/MentraOtaServerModule.web.d.ts +10 -0
- package/build/ota-server/MentraOtaServerModule.web.d.ts.map +1 -0
- package/build/ota-server/MentraOtaServerModule.web.js +12 -0
- package/build/ota-server/MentraOtaServerModule.web.js.map +1 -0
- package/build/ota-server/index.d.ts +3 -0
- package/build/ota-server/index.d.ts.map +1 -0
- package/build/ota-server/index.js +3 -0
- package/build/ota-server/index.js.map +1 -0
- package/build/ota-transport/index.d.ts +67 -0
- package/build/ota-transport/index.d.ts.map +1 -0
- package/build/ota-transport/index.js +58 -0
- package/build/ota-transport/index.js.map +1 -0
- package/expo-module.config.json +3 -2
- package/ios/BackgroundOtaArtifactDownloader.swift +125 -0
- package/ios/BluetoothSdkModule.swift +16 -2
- package/ios/LocalIPv4.swift +150 -0
- package/ios/LocalOtaServer.swift +309 -0
- package/ios/MentraBluetoothSDK.podspec +5 -1
- package/ios/MentraOtaServerModule.swift +147 -0
- package/ios/MentraPhotoReceiverModule.swift +1 -81
- package/ios/Source/BluetoothSdkDefaults.swift +4 -1
- package/ios/Source/Bridge.swift +43 -4
- package/ios/Source/DeviceManager.swift +154 -19
- package/ios/Source/DeviceStore.swift +2 -0
- package/ios/Source/GeneratedChangelogCatalog.swift +6 -0
- package/ios/Source/GeneratedReleaseMetadata.swift +11 -0
- package/ios/Source/MentraBluetoothSDK.swift +91 -6
- package/ios/Source/OtaManifest.swift +5 -7
- package/ios/Source/ReleaseChangelog.swift +65 -0
- package/ios/Source/audio/AudioModels.swift +17 -0
- package/ios/Source/camera/CameraModels.swift +1 -0
- package/ios/Source/events/BluetoothEvents.swift +27 -0
- package/ios/Source/sgcs/MentraLive.swift +512 -78
- package/ios/Source/sgcs/MentraLivePairingAdvertisement.swift +64 -0
- package/ios/Source/sgcs/SGCManager.swift +11 -0
- package/ios/Source/status/DeviceStatus.swift +23 -0
- package/ios/Source/types/DeviceModels.swift +23 -2
- package/ios/Source/utils/BleJsonCompact.swift +73 -71
- package/ios/Tests/MentraLivePairingAdvertisementTests.swift +160 -0
- package/ios/Tests/ReleaseChangelogTests.swift +13 -0
- package/package.json +10 -1
- package/plugin/build/withIos.d.ts +3 -1
- package/plugin/build/withIos.js +22 -10
- package/scripts/public-ota-api.test.mjs +37 -0
- package/scripts/verify-release-package.mjs +34 -0
- package/scripts/write-release-metadata.mjs +154 -0
- package/scripts/write-release-metadata.test.mjs +29 -0
- package/src/BluetoothSdk.types.ts +97 -3
- package/src/_internal.ts +2 -0
- package/src/_private/BluetoothSdkModule.ts +5 -5
- package/src/_private/MentraLocalNetworkModule.ts +1 -1
- package/src/_private/sdkOtaManifest.ts +5 -17
- package/src/changelogs.ts +44 -0
- package/src/generated/changelogCatalog.ts +9 -0
- package/src/generated/releaseMetadata.ts +20 -0
- package/src/index.ts +43 -2
- package/src/ota-server/MentraOtaServer.types.ts +10 -0
- package/src/ota-server/MentraOtaServerModule.ts +23 -0
- package/src/ota-server/MentraOtaServerModule.web.ts +17 -0
- package/src/ota-server/index.ts +2 -0
- package/src/ota-transport/index.ts +116 -0
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
package com.mentra.bluetoothsdk
|
|
2
2
|
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.app.Application
|
|
3
5
|
import android.bluetooth.BluetoothManager
|
|
4
6
|
import android.content.Context
|
|
7
|
+
import android.os.Bundle
|
|
5
8
|
import android.os.Handler
|
|
6
9
|
import android.os.Looper
|
|
7
10
|
import com.mentra.bluetoothsdk.utils.ControllerTypes
|
|
@@ -30,6 +33,23 @@ class MentraBluetoothSdk private constructor(
|
|
|
30
33
|
private val discoveredDeviceNames = mutableSetOf<String>()
|
|
31
34
|
private val bridgeEventSinkId: String
|
|
32
35
|
private val storeListenerId: String
|
|
36
|
+
private val activityLifecycleCallbacks =
|
|
37
|
+
object : Application.ActivityLifecycleCallbacks {
|
|
38
|
+
override fun onActivityResumed(activity: Activity) {
|
|
39
|
+
// Match the Mentra App's OnActivityEntersForeground hook. This runs after
|
|
40
|
+
// runtime permission dialogs in native host apps, allowing the service to
|
|
41
|
+
// replace its temporary dataSync type with connectedDevice/microphone/location.
|
|
42
|
+
deviceManager.refreshForegroundServiceTypes()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) = Unit
|
|
46
|
+
override fun onActivityStarted(activity: Activity) = Unit
|
|
47
|
+
override fun onActivityPaused(activity: Activity) = Unit
|
|
48
|
+
override fun onActivityStopped(activity: Activity) = Unit
|
|
49
|
+
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit
|
|
50
|
+
override fun onActivityDestroyed(activity: Activity) = Unit
|
|
51
|
+
}
|
|
52
|
+
private var activityLifecycleCallbacksRegistered = false
|
|
33
53
|
private var suppressDefaultDeviceEvents = false
|
|
34
54
|
private val streamKeepAliveLock = Any()
|
|
35
55
|
private var activeStreamKeepAlive: ActiveStreamKeepAlive? = null
|
|
@@ -57,6 +77,10 @@ class MentraBluetoothSdk private constructor(
|
|
|
57
77
|
listeners.add(listener)
|
|
58
78
|
Bridge.initialize(appContext)
|
|
59
79
|
deviceManager = DeviceManager.getInstance()
|
|
80
|
+
(appContext as? Application)?.let { application ->
|
|
81
|
+
application.registerActivityLifecycleCallbacks(activityLifecycleCallbacks)
|
|
82
|
+
activityLifecycleCallbacksRegistered = true
|
|
83
|
+
}
|
|
60
84
|
bridgeEventSinkId = Bridge.addEventSink { eventName, data -> dispatchBridgeEvent(eventName, data) }
|
|
61
85
|
// Baseline the analytics connection state before subscribing to the store:
|
|
62
86
|
// store updates invoke listeners synchronously on the updating thread, so a
|
|
@@ -258,6 +282,9 @@ class MentraBluetoothSdk private constructor(
|
|
|
258
282
|
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "device_name", device.name)
|
|
259
283
|
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "device_address", device.address ?: "")
|
|
260
284
|
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "project_name", device.projectName ?: "")
|
|
285
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_name", "")
|
|
286
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_address", "")
|
|
287
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_secure_pairing_capable", "")
|
|
261
288
|
} finally {
|
|
262
289
|
suppressDefaultDeviceEvents = false
|
|
263
290
|
}
|
|
@@ -271,6 +298,9 @@ class MentraBluetoothSdk private constructor(
|
|
|
271
298
|
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "device_name", "")
|
|
272
299
|
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "device_address", "")
|
|
273
300
|
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "project_name", "")
|
|
301
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_name", "")
|
|
302
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_address", "")
|
|
303
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_secure_pairing_capable", "")
|
|
274
304
|
} finally {
|
|
275
305
|
suppressDefaultDeviceEvents = false
|
|
276
306
|
}
|
|
@@ -383,8 +413,29 @@ class MentraBluetoothSdk private constructor(
|
|
|
383
413
|
cancelConnectionAttempt()
|
|
384
414
|
}
|
|
385
415
|
}
|
|
386
|
-
if (
|
|
387
|
-
|
|
416
|
+
if (!isController) {
|
|
417
|
+
if (options.saveAsDefault) {
|
|
418
|
+
setDefaultDevice(device)
|
|
419
|
+
} else {
|
|
420
|
+
// Pairing uses saveAsDefault=false so default identity stays on the
|
|
421
|
+
// previous owner until handleDeviceReady. Stash the GATT target separately
|
|
422
|
+
// so MentraLive can connect by MAC without promoting getDefaultDevice().
|
|
423
|
+
DeviceStore.apply(
|
|
424
|
+
ObservableStore.BLUETOOTH_CATEGORY,
|
|
425
|
+
"pending_device_name",
|
|
426
|
+
device.name,
|
|
427
|
+
)
|
|
428
|
+
DeviceStore.apply(
|
|
429
|
+
ObservableStore.BLUETOOTH_CATEGORY,
|
|
430
|
+
"pending_device_address",
|
|
431
|
+
device.address ?: "",
|
|
432
|
+
)
|
|
433
|
+
DeviceStore.apply(
|
|
434
|
+
ObservableStore.BLUETOOTH_CATEGORY,
|
|
435
|
+
"pending_device_secure_pairing_capable",
|
|
436
|
+
device.securePairingCapable ?: "",
|
|
437
|
+
)
|
|
438
|
+
}
|
|
388
439
|
}
|
|
389
440
|
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_wearable", device.model.deviceType)
|
|
390
441
|
deviceManager.connectByName(device.name)
|
|
@@ -408,6 +459,9 @@ class MentraBluetoothSdk private constructor(
|
|
|
408
459
|
}
|
|
409
460
|
|
|
410
461
|
fun cancelConnectionAttempt() {
|
|
462
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_name", "")
|
|
463
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_address", "")
|
|
464
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_secure_pairing_capable", "")
|
|
411
465
|
deviceManager.disconnect()
|
|
412
466
|
}
|
|
413
467
|
|
|
@@ -416,6 +470,9 @@ class MentraBluetoothSdk private constructor(
|
|
|
416
470
|
}
|
|
417
471
|
|
|
418
472
|
fun disconnect() {
|
|
473
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_name", "")
|
|
474
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_address", "")
|
|
475
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "pending_device_secure_pairing_capable", "")
|
|
419
476
|
deviceManager.disconnect()
|
|
420
477
|
}
|
|
421
478
|
|
|
@@ -856,6 +913,10 @@ class MentraBluetoothSdk private constructor(
|
|
|
856
913
|
}
|
|
857
914
|
}
|
|
858
915
|
|
|
916
|
+
fun setWifiAdbState(enabled: Boolean) {
|
|
917
|
+
deviceManager.setWifiAdbState(enabled)
|
|
918
|
+
}
|
|
919
|
+
|
|
859
920
|
fun setSystemTime(timestampMs: Long) {
|
|
860
921
|
deviceManager.setSystemTime(timestampMs)
|
|
861
922
|
}
|
|
@@ -1087,6 +1148,25 @@ class MentraBluetoothSdk private constructor(
|
|
|
1087
1148
|
}
|
|
1088
1149
|
}
|
|
1089
1150
|
|
|
1151
|
+
suspend fun queryVideoRecordingStatus(requestId: String): VideoRecordingStatusEvent {
|
|
1152
|
+
require(requestId.isNotBlank()) { "requestId is required to query video recording status." }
|
|
1153
|
+
requireGlassesConnected("query video recording status")
|
|
1154
|
+
val pending = PendingResponse<VideoRecordingStatusEvent>("query video recording status")
|
|
1155
|
+
val pendingRequest = PendingVideoRecordingRequest("recording_status", pending)
|
|
1156
|
+
if (pendingVideoRecordingRequests.putIfAbsent(requestId, pendingRequest) != null) {
|
|
1157
|
+
throw BluetoothSdkException(
|
|
1158
|
+
"request_in_flight",
|
|
1159
|
+
"A video recording command is already waiting for requestId $requestId.",
|
|
1160
|
+
)
|
|
1161
|
+
}
|
|
1162
|
+
try {
|
|
1163
|
+
deviceManager.queryVideoRecordingStatus(requestId)
|
|
1164
|
+
return pending.await()
|
|
1165
|
+
} finally {
|
|
1166
|
+
pendingVideoRecordingRequests.remove(requestId, pendingRequest)
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1090
1170
|
suspend fun requestVersionInfo(): VersionInfoResult {
|
|
1091
1171
|
val pending = PendingResponse<VersionInfoResult>("version info request")
|
|
1092
1172
|
synchronized(oneShotLock) {
|
|
@@ -1110,11 +1190,13 @@ class MentraBluetoothSdk private constructor(
|
|
|
1110
1190
|
}
|
|
1111
1191
|
}
|
|
1112
1192
|
|
|
1113
|
-
|
|
1193
|
+
/** Select the HTTP(S) manifest used by subsequent OTA checks and installs. */
|
|
1194
|
+
fun setOtaVersionUrl(otaVersionUrl: String) {
|
|
1114
1195
|
configuredOtaVersionUrl = OtaManifestChecker.normalizeHttpUrl(otaVersionUrl)
|
|
1115
1196
|
}
|
|
1116
1197
|
|
|
1117
|
-
|
|
1198
|
+
/** Return the configured OTA manifest, or this SDK release's default manifest. */
|
|
1199
|
+
fun getOtaVersionUrl(): String = configuredOtaVersionUrl ?: OtaManifestDefaults.defaultOtaVersionUrl()
|
|
1118
1200
|
|
|
1119
1201
|
/** Fetch the configured OTA manifest and return whether any ASG/BES/MTK update is available. */
|
|
1120
1202
|
suspend fun checkForOtaUpdate(): Boolean {
|
|
@@ -1143,6 +1225,12 @@ class MentraBluetoothSdk private constructor(
|
|
|
1143
1225
|
)
|
|
1144
1226
|
}
|
|
1145
1227
|
|
|
1228
|
+
/** Return bundled release changelogs crossed between two coordinated product versions, newest first. */
|
|
1229
|
+
fun getReleaseChangelogs(
|
|
1230
|
+
fromVersion: String? = null,
|
|
1231
|
+
toVersion: String? = null,
|
|
1232
|
+
): List<ReleaseChangelog> = ReleaseChangelogCatalog.select(fromVersion, toVersion)
|
|
1233
|
+
|
|
1146
1234
|
/** Ask connected Mentra Live glasses to report the current OTA install/session status. */
|
|
1147
1235
|
private suspend fun queryOtaStatus(): OtaQueryResult =
|
|
1148
1236
|
performOtaQuery("OTA status query") {
|
|
@@ -1240,6 +1328,12 @@ class MentraBluetoothSdk private constructor(
|
|
|
1240
1328
|
systemTimeMs = versionInfo.systemTimeMs ?: status.systemTimeMs,
|
|
1241
1329
|
otaVersionUrl = versionInfo.otaVersionUrl.ifBlank { status.otaVersionUrl },
|
|
1242
1330
|
appVersion = versionInfo.appVersion.ifBlank { status.appVersion },
|
|
1331
|
+
hotspotOtaVersion =
|
|
1332
|
+
if (versionInfo.hotspotOtaVersion > 0) {
|
|
1333
|
+
versionInfo.hotspotOtaVersion
|
|
1334
|
+
} else {
|
|
1335
|
+
status.hotspotOtaVersion
|
|
1336
|
+
},
|
|
1243
1337
|
)
|
|
1244
1338
|
} catch (cancellation: CancellationException) {
|
|
1245
1339
|
// Never swallow cancellation into the stale-status fallback: callers
|
|
@@ -1323,6 +1417,12 @@ class MentraBluetoothSdk private constructor(
|
|
|
1323
1417
|
|
|
1324
1418
|
override fun close() {
|
|
1325
1419
|
stopStreamKeepAliveMonitor()
|
|
1420
|
+
if (activityLifecycleCallbacksRegistered) {
|
|
1421
|
+
(appContext as? Application)?.unregisterActivityLifecycleCallbacks(
|
|
1422
|
+
activityLifecycleCallbacks,
|
|
1423
|
+
)
|
|
1424
|
+
activityLifecycleCallbacksRegistered = false
|
|
1425
|
+
}
|
|
1326
1426
|
Bridge.removeEventSink(bridgeEventSinkId)
|
|
1327
1427
|
DeviceStore.store.removeListener(storeListenerId)
|
|
1328
1428
|
analytics.shutdown()
|
|
@@ -1520,6 +1620,9 @@ class MentraBluetoothSdk private constructor(
|
|
|
1520
1620
|
}
|
|
1521
1621
|
dispatchToListeners { it.onGalleryStatus(event) }
|
|
1522
1622
|
}
|
|
1623
|
+
"pairing_info" -> {
|
|
1624
|
+
dispatchToListeners { it.onRawEvent(eventName, data) }
|
|
1625
|
+
}
|
|
1523
1626
|
"photo_response" -> {
|
|
1524
1627
|
val event = PhotoResponseEvent(data)
|
|
1525
1628
|
handlePhotoResponseForRequests(event)
|
|
@@ -1567,10 +1670,19 @@ class MentraBluetoothSdk private constructor(
|
|
|
1567
1670
|
}
|
|
1568
1671
|
"ota_status" -> {
|
|
1569
1672
|
val resultValues = data + mapOf("type" to "ota_status")
|
|
1673
|
+
val event = OtaStatusEvent.fromMap(resultValues)
|
|
1570
1674
|
synchronized(oneShotLock) {
|
|
1571
1675
|
pendingOtaQuery?.resolve(OtaQueryResult(resultValues))
|
|
1676
|
+
otaStartRejectionErrorCode(event)?.let { errorCode ->
|
|
1677
|
+
pendingOtaStart?.reject(
|
|
1678
|
+
BluetoothSdkException(errorCode, "Glasses rejected OTA start: $errorCode")
|
|
1679
|
+
)
|
|
1680
|
+
}
|
|
1572
1681
|
}
|
|
1573
|
-
dispatchToListeners { it.onOtaStatus(
|
|
1682
|
+
dispatchToListeners { it.onOtaStatus(event) }
|
|
1683
|
+
}
|
|
1684
|
+
"mic_health" -> {
|
|
1685
|
+
dispatchToListeners { it.onMicHealth(MicHealthEvent.fromMap(data)) }
|
|
1574
1686
|
}
|
|
1575
1687
|
"settings_ack" -> {
|
|
1576
1688
|
val event = SettingsAckEvent(data)
|
|
@@ -2161,6 +2273,13 @@ class MentraBluetoothSdk private constructor(
|
|
|
2161
2273
|
}
|
|
2162
2274
|
}
|
|
2163
2275
|
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2276
|
+
/** OTA status messages are not request-correlated, so only known pre-ack failures settle a start. */
|
|
2277
|
+
internal fun otaStartRejectionErrorCode(event: OtaStatusEvent): String? =
|
|
2278
|
+
if (
|
|
2279
|
+
event.status.equals("failed", ignoreCase = true) &&
|
|
2280
|
+
event.errorMessage.equals("battery_low", ignoreCase = true)
|
|
2281
|
+
) {
|
|
2282
|
+
"battery_low"
|
|
2283
|
+
} else {
|
|
2284
|
+
null
|
|
2285
|
+
}
|
|
@@ -5,7 +5,8 @@ import android.net.Network
|
|
|
5
5
|
import android.net.NetworkCapabilities
|
|
6
6
|
import android.net.NetworkRequest
|
|
7
7
|
import android.net.wifi.WifiNetworkSpecifier
|
|
8
|
-
import android.os.
|
|
8
|
+
import android.os.Handler
|
|
9
|
+
import android.os.Looper
|
|
9
10
|
import android.util.Base64
|
|
10
11
|
import android.util.Log
|
|
11
12
|
import expo.modules.kotlin.Promise
|
|
@@ -14,6 +15,7 @@ import expo.modules.kotlin.modules.ModuleDefinition
|
|
|
14
15
|
import java.io.File
|
|
15
16
|
import java.io.FileOutputStream
|
|
16
17
|
import java.net.HttpURLConnection
|
|
18
|
+
import java.net.Inet4Address
|
|
17
19
|
import java.net.URL
|
|
18
20
|
import java.util.concurrent.ConcurrentHashMap
|
|
19
21
|
import java.util.concurrent.Executors
|
|
@@ -22,14 +24,18 @@ import java.util.concurrent.Executors
|
|
|
22
24
|
class MentraLocalNetworkModule : Module() {
|
|
23
25
|
private companion object {
|
|
24
26
|
const val TAG = "MentraLocalNetwork"
|
|
27
|
+
const val NETWORK_READINESS_TIMEOUT_MS = 5_000L
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
private val lock = Any()
|
|
28
31
|
private val executor = Executors.newCachedThreadPool()
|
|
32
|
+
private val handler = Handler(Looper.getMainLooper())
|
|
29
33
|
private val activeConnections = ConcurrentHashMap<String, HttpURLConnection>()
|
|
34
|
+
private val networkReadiness = ScopedNetworkReadiness<Network>()
|
|
30
35
|
private var localNetwork: Network? = null
|
|
31
36
|
private var networkCallback: ConnectivityManager.NetworkCallback? = null
|
|
32
37
|
private var pendingConnectPromise: Promise? = null
|
|
38
|
+
private var pendingReadinessTimeout: Runnable? = null
|
|
33
39
|
|
|
34
40
|
override fun definition() = ModuleDefinition {
|
|
35
41
|
Name("MentraLocalNetwork")
|
|
@@ -74,7 +80,9 @@ class MentraLocalNetworkModule : Module() {
|
|
|
74
80
|
AsyncFunction("cancel") { requestId: String -> cancel(requestId) }
|
|
75
81
|
AsyncFunction("disconnect") { disconnect() }
|
|
76
82
|
|
|
77
|
-
OnDestroy {
|
|
83
|
+
OnDestroy {
|
|
84
|
+
disconnect()
|
|
85
|
+
}
|
|
78
86
|
}
|
|
79
87
|
|
|
80
88
|
private fun connectivityManager(): ConnectivityManager =
|
|
@@ -83,11 +91,6 @@ class MentraLocalNetworkModule : Module() {
|
|
|
83
91
|
}
|
|
84
92
|
|
|
85
93
|
private fun connect(ssid: String, password: String, promise: Promise) {
|
|
86
|
-
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
|
87
|
-
promise.reject("ERR_UNSUPPORTED_ANDROID", "Scoped local WiFi requires Android 10 or newer", null)
|
|
88
|
-
return
|
|
89
|
-
}
|
|
90
|
-
|
|
91
94
|
disconnect()
|
|
92
95
|
val manager = connectivityManager()
|
|
93
96
|
val specifier =
|
|
@@ -105,21 +108,55 @@ class MentraLocalNetworkModule : Module() {
|
|
|
105
108
|
val callback =
|
|
106
109
|
object : ConnectivityManager.NetworkCallback() {
|
|
107
110
|
override fun onAvailable(network: Network) {
|
|
108
|
-
val connectPromise: Promise?
|
|
109
111
|
synchronized(lock) {
|
|
110
112
|
if (networkCallback !== this) {
|
|
111
113
|
Log.d(TAG, "Ignoring onAvailable from a stale network request")
|
|
112
114
|
return
|
|
113
115
|
}
|
|
116
|
+
networkReadiness.onAvailable(network)
|
|
117
|
+
}
|
|
118
|
+
Log.i(
|
|
119
|
+
TAG,
|
|
120
|
+
"Captured glasses WiFi network=$network; waiting for foreground capability " +
|
|
121
|
+
"processBoundNetwork=${manager.boundNetworkForProcess}",
|
|
122
|
+
)
|
|
123
|
+
scheduleReadinessTimeout(this, ssid)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
override fun onCapabilitiesChanged(network: Network, capabilities: NetworkCapabilities) {
|
|
127
|
+
val connectPromise: Promise?
|
|
128
|
+
synchronized(lock) {
|
|
129
|
+
if (networkCallback !== this || localNetwork != null || pendingConnectPromise == null) return
|
|
130
|
+
if (!networkReadiness.isReady(
|
|
131
|
+
network,
|
|
132
|
+
capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_FOREGROUND),
|
|
133
|
+
)
|
|
134
|
+
) {
|
|
135
|
+
return
|
|
136
|
+
}
|
|
114
137
|
localNetwork = network
|
|
115
138
|
connectPromise = pendingConnectPromise
|
|
116
139
|
pendingConnectPromise = null
|
|
140
|
+
cancelReadinessTimeoutLocked()
|
|
117
141
|
}
|
|
118
142
|
Log.i(
|
|
119
143
|
TAG,
|
|
120
|
-
"
|
|
144
|
+
"Glasses WiFi network=$network is foreground and ready " +
|
|
145
|
+
"processBoundNetwork=${manager.boundNetworkForProcess}",
|
|
146
|
+
)
|
|
147
|
+
val localAddress =
|
|
148
|
+
manager.getLinkProperties(network)
|
|
149
|
+
?.linkAddresses
|
|
150
|
+
?.firstOrNull { it.address is Inet4Address }
|
|
151
|
+
?.address
|
|
152
|
+
?.hostAddress
|
|
153
|
+
connectPromise?.resolve(
|
|
154
|
+
buildMap<String, Any> {
|
|
155
|
+
put("connected", true)
|
|
156
|
+
put("ssid", ssid)
|
|
157
|
+
if (!localAddress.isNullOrBlank()) put("localAddress", localAddress)
|
|
158
|
+
},
|
|
121
159
|
)
|
|
122
|
-
connectPromise?.resolve(mapOf("connected" to true, "ssid" to ssid))
|
|
123
160
|
}
|
|
124
161
|
|
|
125
162
|
override fun onUnavailable() {
|
|
@@ -135,13 +172,17 @@ class MentraLocalNetworkModule : Module() {
|
|
|
135
172
|
|
|
136
173
|
override fun onLost(network: Network) {
|
|
137
174
|
synchronized(lock) {
|
|
138
|
-
if (networkCallback !== this ||
|
|
175
|
+
if (networkCallback !== this ||
|
|
176
|
+
(localNetwork != network && !networkReadiness.contains(network))
|
|
177
|
+
) {
|
|
139
178
|
Log.d(TAG, "Ignoring onLost from a stale network request")
|
|
140
179
|
return
|
|
141
180
|
}
|
|
142
181
|
localNetwork = null
|
|
182
|
+
networkReadiness.clear()
|
|
143
183
|
}
|
|
144
184
|
cancelAll()
|
|
185
|
+
rejectPendingConnect("ERR_LOCAL_WIFI_LOST", "Lost connection to $ssid")
|
|
145
186
|
clearNetworkCallback(this)
|
|
146
187
|
sendEvent("networkLost", mapOf("ssid" to ssid))
|
|
147
188
|
}
|
|
@@ -289,6 +330,39 @@ class MentraLocalNetworkModule : Module() {
|
|
|
289
330
|
activeConnections.keys.toList().forEach(::cancel)
|
|
290
331
|
}
|
|
291
332
|
|
|
333
|
+
private fun scheduleReadinessTimeout(
|
|
334
|
+
callback: ConnectivityManager.NetworkCallback,
|
|
335
|
+
ssid: String,
|
|
336
|
+
) {
|
|
337
|
+
val timeout =
|
|
338
|
+
Runnable {
|
|
339
|
+
val connectPromise = synchronized(lock) {
|
|
340
|
+
if (networkCallback !== callback || pendingConnectPromise == null) return@Runnable
|
|
341
|
+
pendingReadinessTimeout = null
|
|
342
|
+
val promise = pendingConnectPromise
|
|
343
|
+
pendingConnectPromise = null
|
|
344
|
+
promise
|
|
345
|
+
}
|
|
346
|
+
connectPromise?.reject(
|
|
347
|
+
"ERR_LOCAL_WIFI_NOT_READY",
|
|
348
|
+
"Connected to $ssid but the network did not become ready",
|
|
349
|
+
null,
|
|
350
|
+
)
|
|
351
|
+
clearNetworkCallback(callback)
|
|
352
|
+
}
|
|
353
|
+
synchronized(lock) {
|
|
354
|
+
if (networkCallback !== callback || pendingConnectPromise == null) return
|
|
355
|
+
cancelReadinessTimeoutLocked()
|
|
356
|
+
pendingReadinessTimeout = timeout
|
|
357
|
+
}
|
|
358
|
+
handler.postDelayed(timeout, NETWORK_READINESS_TIMEOUT_MS)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private fun cancelReadinessTimeoutLocked() {
|
|
362
|
+
pendingReadinessTimeout?.let(handler::removeCallbacks)
|
|
363
|
+
pendingReadinessTimeout = null
|
|
364
|
+
}
|
|
365
|
+
|
|
292
366
|
private fun disconnect() {
|
|
293
367
|
cancelAll()
|
|
294
368
|
val manager = appContext.reactContext?.getSystemService(ConnectivityManager::class.java)
|
|
@@ -297,6 +371,8 @@ class MentraLocalNetworkModule : Module() {
|
|
|
297
371
|
callback = networkCallback
|
|
298
372
|
networkCallback = null
|
|
299
373
|
localNetwork = null
|
|
374
|
+
networkReadiness.clear()
|
|
375
|
+
cancelReadinessTimeoutLocked()
|
|
300
376
|
}
|
|
301
377
|
if (callback != null && manager != null) {
|
|
302
378
|
try {
|
|
@@ -311,7 +387,12 @@ class MentraLocalNetworkModule : Module() {
|
|
|
311
387
|
private fun clearNetworkCallback(callback: ConnectivityManager.NetworkCallback) {
|
|
312
388
|
val manager = appContext.reactContext?.getSystemService(ConnectivityManager::class.java)
|
|
313
389
|
synchronized(lock) {
|
|
314
|
-
if (networkCallback == callback)
|
|
390
|
+
if (networkCallback == callback) {
|
|
391
|
+
networkCallback = null
|
|
392
|
+
localNetwork = null
|
|
393
|
+
networkReadiness.clear()
|
|
394
|
+
cancelReadinessTimeoutLocked()
|
|
395
|
+
}
|
|
315
396
|
}
|
|
316
397
|
if (manager != null) {
|
|
317
398
|
try {
|
|
@@ -7,21 +7,19 @@ import org.json.JSONArray
|
|
|
7
7
|
import org.json.JSONObject
|
|
8
8
|
|
|
9
9
|
internal object OtaManifestDefaults {
|
|
10
|
-
private const val SDK_OTA_RELEASE_BASE_URL =
|
|
11
|
-
"https://github.com/Mentra-Community/MentraOS/releases/download/bluetooth-sdk-ota"
|
|
12
10
|
// ASG builds before 39 ignore ota_start.ota_version_url, so SDK checks must
|
|
13
11
|
// use the same legacy production manifest those glasses will install from.
|
|
14
12
|
const val LEGACY_PROD_OTA_VERSION_URL = "https://ota.mentraglass.com/prod_live_version.json"
|
|
15
13
|
|
|
16
14
|
fun defaultOtaVersionUrl(): String {
|
|
17
|
-
val
|
|
18
|
-
if (
|
|
15
|
+
val manifestUrl = GeneratedReleaseMetadata.OTA_MANIFEST_URL.trim()
|
|
16
|
+
if (manifestUrl.isBlank()) {
|
|
19
17
|
throw BluetoothSdkException(
|
|
20
|
-
"
|
|
21
|
-
"
|
|
18
|
+
"ota_manifest_unconfigured",
|
|
19
|
+
"This source-built Bluetooth SDK has no embedded OTA manifest. Configure one with the debug API.",
|
|
22
20
|
)
|
|
23
21
|
}
|
|
24
|
-
return
|
|
22
|
+
return manifestUrl
|
|
25
23
|
}
|
|
26
24
|
}
|
|
27
25
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk
|
|
2
|
+
|
|
3
|
+
/** One coordinated Mentra release note authored in /changelogs/<version>.md. */
|
|
4
|
+
data class ReleaseChangelog(
|
|
5
|
+
val version: String,
|
|
6
|
+
val markdown: String,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
internal object ReleaseChangelogCatalog {
|
|
10
|
+
private val baseVersionPattern = Regex("^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:[-+].*)?$")
|
|
11
|
+
|
|
12
|
+
fun select(
|
|
13
|
+
fromVersion: String?,
|
|
14
|
+
toVersion: String?,
|
|
15
|
+
): List<ReleaseChangelog> {
|
|
16
|
+
val fallbackTarget =
|
|
17
|
+
GeneratedReleaseMetadata.FAMILY_BASE_VERSION.ifBlank {
|
|
18
|
+
GENERATED_RELEASE_CHANGELOGS.firstOrNull()?.version.orEmpty()
|
|
19
|
+
}
|
|
20
|
+
if (fallbackTarget.isBlank() && toVersion == null) return emptyList()
|
|
21
|
+
val target = baseVersion(toVersion ?: fallbackTarget, "toVersion")
|
|
22
|
+
if (GENERATED_RELEASE_CHANGELOGS.none { it.version == target }) {
|
|
23
|
+
throw BluetoothSdkException("missing_changelog", "No changelog is bundled for target version $target.")
|
|
24
|
+
}
|
|
25
|
+
if (fromVersion == null) return GENERATED_RELEASE_CHANGELOGS.filter { it.version == target }
|
|
26
|
+
|
|
27
|
+
val source = baseVersion(fromVersion, "fromVersion")
|
|
28
|
+
val direction = compareVersions(target, source)
|
|
29
|
+
return GENERATED_RELEASE_CHANGELOGS.filter { entry ->
|
|
30
|
+
when {
|
|
31
|
+
direction == 0 -> entry.version == target
|
|
32
|
+
direction > 0 -> compareVersions(entry.version, source) > 0 && compareVersions(entry.version, target) <= 0
|
|
33
|
+
else -> compareVersions(entry.version, source) < 0 && compareVersions(entry.version, target) >= 0
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private fun baseVersion(
|
|
39
|
+
version: String,
|
|
40
|
+
label: String,
|
|
41
|
+
): String {
|
|
42
|
+
val match = baseVersionPattern.matchEntire(version.trim())
|
|
43
|
+
?: throw BluetoothSdkException(
|
|
44
|
+
"invalid_changelog_version",
|
|
45
|
+
"$label must be a semantic version such as 3.1.0, 3.1.0-dev.4, or 3.1.0-beta.2.",
|
|
46
|
+
)
|
|
47
|
+
return "${match.groupValues[1]}.${match.groupValues[2]}.${match.groupValues[3]}"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private fun compareVersions(
|
|
51
|
+
left: String,
|
|
52
|
+
right: String,
|
|
53
|
+
): Int {
|
|
54
|
+
val a = left.split(".").map(String::toInt)
|
|
55
|
+
val b = right.split(".").map(String::toInt)
|
|
56
|
+
for (index in 0..2) {
|
|
57
|
+
if (a[index] != b[index]) return a[index] - b[index]
|
|
58
|
+
}
|
|
59
|
+
return 0
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk
|
|
2
|
+
|
|
3
|
+
internal class ScopedNetworkReadiness<T> {
|
|
4
|
+
private var candidate: T? = null
|
|
5
|
+
|
|
6
|
+
fun onAvailable(network: T) {
|
|
7
|
+
candidate = network
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
fun isReady(network: T, isForeground: Boolean): Boolean = candidate == network && isForeground
|
|
11
|
+
|
|
12
|
+
fun contains(network: T): Boolean = candidate == network
|
|
13
|
+
|
|
14
|
+
fun clear() {
|
|
15
|
+
candidate = null
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -94,6 +94,21 @@ data class MicLc3Event(
|
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
internal data class MicHealth(
|
|
98
|
+
val sequenceGapEvents: Long,
|
|
99
|
+
val decodeFailures: Long,
|
|
100
|
+
val lastLc3ReceivedAt: Long?,
|
|
101
|
+
val lastPcmProducedAt: Long?,
|
|
102
|
+
) {
|
|
103
|
+
fun toMap(): Map<String, Any> =
|
|
104
|
+
buildMap {
|
|
105
|
+
put("sequenceGapEvents", sequenceGapEvents)
|
|
106
|
+
put("decodeFailures", decodeFailures)
|
|
107
|
+
lastLc3ReceivedAt?.let { put("lastLc3ReceivedAt", it) }
|
|
108
|
+
lastPcmProducedAt?.let { put("lastPcmProducedAt", it) }
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
97
112
|
data class LocalTranscriptionEvent(
|
|
98
113
|
val text: String,
|
|
99
114
|
val isFinal: Boolean,
|
|
@@ -39,6 +39,29 @@ data class SpeakingStatusEvent(
|
|
|
39
39
|
val values: Map<String, Any>,
|
|
40
40
|
)
|
|
41
41
|
|
|
42
|
+
data class MicHealthEvent(
|
|
43
|
+
val reason: String,
|
|
44
|
+
val sequenceGapEvents: Long,
|
|
45
|
+
val decodeFailures: Long,
|
|
46
|
+
val lastLc3ReceivedAt: Long?,
|
|
47
|
+
val lastPcmProducedAt: Long?,
|
|
48
|
+
val timestamp: Long,
|
|
49
|
+
val values: Map<String, Any>,
|
|
50
|
+
) {
|
|
51
|
+
companion object {
|
|
52
|
+
internal fun fromMap(values: Map<String, Any>): MicHealthEvent =
|
|
53
|
+
MicHealthEvent(
|
|
54
|
+
reason = stringValue(values, "reason") ?: "",
|
|
55
|
+
sequenceGapEvents = longValue(values, "sequenceGapEvents") ?: 0,
|
|
56
|
+
decodeFailures = longValue(values, "decodeFailures") ?: 0,
|
|
57
|
+
lastLc3ReceivedAt = longValue(values, "lastLc3ReceivedAt"),
|
|
58
|
+
lastPcmProducedAt = longValue(values, "lastPcmProducedAt"),
|
|
59
|
+
timestamp = longValue(values, "timestamp") ?: System.currentTimeMillis(),
|
|
60
|
+
values = values,
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
42
65
|
data class OtaStartAckEvent(
|
|
43
66
|
val timestamp: Long?,
|
|
44
67
|
val values: Map<String, Any>,
|
|
@@ -144,6 +167,7 @@ interface MentraBluetoothSdkListener {
|
|
|
144
167
|
fun onVersionInfo(event: VersionInfoResult) {}
|
|
145
168
|
fun onMicPcm(event: MicPcmEvent) {}
|
|
146
169
|
fun onMicLc3(event: MicLc3Event) {}
|
|
170
|
+
fun onMicHealth(event: MicHealthEvent) {}
|
|
147
171
|
fun onLocalTranscription(event: LocalTranscriptionEvent) {}
|
|
148
172
|
fun onDefaultDeviceChanged(device: Device?) {}
|
|
149
173
|
fun onLog(message: String) {}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.net
|
|
2
|
+
|
|
3
|
+
import java.net.Inet4Address
|
|
4
|
+
import java.net.NetworkInterface
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Best-effort local IPv4 discovery for phone-hosted servers the glasses reach over WiFi
|
|
8
|
+
* (photo upload receiver, hotspot-served OTA). Prefers WiFi-ish interfaces carrying RFC1918
|
|
9
|
+
* addresses — under a glasses-hotspot session wlan0 holds the 192.168.43.x client address.
|
|
10
|
+
*/
|
|
11
|
+
object LocalIpv4 {
|
|
12
|
+
fun bestLocalIpv4Address(): String? {
|
|
13
|
+
val wifiPrivateCandidates = mutableListOf<Inet4Address>()
|
|
14
|
+
val wifiCandidates = mutableListOf<Inet4Address>()
|
|
15
|
+
val privateCandidates = mutableListOf<Inet4Address>()
|
|
16
|
+
val otherCandidates = mutableListOf<Inet4Address>()
|
|
17
|
+
val interfaces = NetworkInterface.getNetworkInterfaces()?.toList().orEmpty()
|
|
18
|
+
for (networkInterface in interfaces) {
|
|
19
|
+
if (!networkInterface.isUp || networkInterface.isLoopback) {
|
|
20
|
+
continue
|
|
21
|
+
}
|
|
22
|
+
val addresses = networkInterface.inetAddresses.toList()
|
|
23
|
+
.filterIsInstance<Inet4Address>()
|
|
24
|
+
.filter { address ->
|
|
25
|
+
!address.isLoopbackAddress &&
|
|
26
|
+
!address.isLinkLocalAddress
|
|
27
|
+
}
|
|
28
|
+
if (isWifiInterface(networkInterface.name)) {
|
|
29
|
+
wifiPrivateCandidates += addresses.filter { isPrivateIpv4(it.hostAddress.orEmpty()) }
|
|
30
|
+
wifiCandidates += addresses
|
|
31
|
+
} else {
|
|
32
|
+
privateCandidates += addresses.filter { isPrivateIpv4(it.hostAddress.orEmpty()) }
|
|
33
|
+
otherCandidates += addresses
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
wifiPrivateCandidates.firstOrNull() ?:
|
|
39
|
+
wifiCandidates.firstOrNull() ?:
|
|
40
|
+
privateCandidates.firstOrNull() ?:
|
|
41
|
+
otherCandidates.firstOrNull()
|
|
42
|
+
)?.hostAddress
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private fun isWifiInterface(name: String): Boolean {
|
|
46
|
+
return name.startsWith("wlan") ||
|
|
47
|
+
name.startsWith("p2p") ||
|
|
48
|
+
name.startsWith("ap") ||
|
|
49
|
+
name.startsWith("swlan")
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private fun isPrivateIpv4(host: String): Boolean {
|
|
53
|
+
return host.startsWith("192.168.") ||
|
|
54
|
+
host.startsWith("10.") ||
|
|
55
|
+
host.matches(Regex("^172\\.(1[6-9]|2[0-9]|3[0-1])\\..*"))
|
|
56
|
+
}
|
|
57
|
+
}
|