@mentra/bluetooth-sdk 3.2.1-dev.314 → 3.3.0-beta.942
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 +72 -10
- package/android/build.gradle +15 -0
- package/android/lc3Lib/build.gradle +6 -1
- package/android/src/debug/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnostics.kt +368 -0
- package/android/src/debug/java/com/mentra/bluetoothsdk/sgcs/NimoFramebufferProtocol.kt +156 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalytics.kt +63 -14
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsTracker.kt +55 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkDefaults.kt +4 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkInstallId.kt +23 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +8 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkUpdateCheck.kt +184 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkUsageTracker.kt +388 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +2 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +133 -26
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +37 -13
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +3 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +6 -6
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +227 -36
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraLocalNetworkModule.kt +24 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/NetworkDiagnostics.kt +25 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulator.kt +13 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/debug/BleTraceLogger.kt +3 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/requests/DisplayRequests.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/services/Foreground.kt +33 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/services/G2ConnectionRecovery.kt +47 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioConnectionTracker.kt +4 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioServiceDiscovery.kt +70 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.kt +2 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizer.kt +11 -32
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +72 -21
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/LatinTextSanitizer.kt +39 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +194 -40
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +12 -8
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Nimo.kt +314 -395
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCodec.kt +247 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCoordinator.kt +238 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasEncoder.kt +58 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasSession.kt +180 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +11 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SelectedDeviceAddress.kt +17 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SerializedGattCallback.kt +5 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/G2Text.kt +7 -0
- package/android/src/release/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnostics.kt +20 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkAnalyticsTrackerTest.kt +58 -3
- package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkUpdateCheckTest.kt +124 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/BluetoothSdkUsageTrackerTest.kt +263 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/DashboardContentFormatterTest.kt +2 -2
- package/android/src/test/java/com/mentra/bluetoothsdk/DeviceIdentityPairingTest.kt +59 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/DeviceManagerSceneHandoffTest.kt +666 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/G2TextTest.kt +13 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/NetworkDiagnosticsTest.kt +55 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/ScanCompletionTest.kt +5 -1
- package/android/src/test/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulatorTest.kt +71 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/debug/BleTraceLoggerTest.kt +69 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/services/G2ConnectionRecoveryTest.kt +51 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioServiceDiscoveryTest.kt +124 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1PairingTest.kt +105 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1TextSanitizerTest.kt +29 -4
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G2DisplayThreadTest.kt +120 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G2PairingTargetTest.kt +90 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveClassicDisconnectTest.kt +133 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveFileTransferSessionTest.kt +55 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveGattCallbackTest.kt +33 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveIncidentDispatchTest.kt +126 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingTargetTest.kt +65 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveWifiCredentialLogTest.kt +55 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraNexPairingTest.kt +151 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoBitmapAcceptanceTest.kt +105 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCodecTest.kt +233 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasCoordinatorTest.kt +348 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoCanvasSessionTest.kt +339 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoMicReconnectTest.kt +93 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/NimoProtocolTest.kt +11 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/SelectedDeviceAddressTest.kt +27 -0
- package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoDiagnosticsTest.kt +287 -0
- package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoFramebufferProtocolTest.kt +127 -0
- package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoLifecycleTest.kt +288 -0
- package/android/src/testDebug/java/com/mentra/bluetoothsdk/sgcs/NimoReadinessDriverTest.kt +205 -0
- package/build/BluetoothSdk.types.d.ts +16 -0
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js +1 -0
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +4 -0
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +5 -1
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/_private/MentraLocalNetworkModule.d.ts +23 -0
- package/build/_private/MentraLocalNetworkModule.d.ts.map +1 -1
- package/build/_private/MentraLocalNetworkModule.js.map +1 -1
- package/build/_private/nativeLogConsole.d.ts +19 -0
- package/build/_private/nativeLogConsole.d.ts.map +1 -1
- package/build/_private/nativeLogConsole.js +12 -0
- package/build/_private/nativeLogConsole.js.map +1 -1
- package/build/generated/changelogCatalog.d.ts +5 -2
- package/build/generated/changelogCatalog.d.ts.map +1 -1
- package/build/generated/changelogCatalog.js +6 -2
- package/build/generated/changelogCatalog.js.map +1 -1
- package/build/generated/releaseMetadata.js +6 -6
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/build/ota-transport/index.d.ts +4 -0
- package/build/ota-transport/index.d.ts.map +1 -1
- package/build/ota-transport/index.js +2 -0
- package/build/ota-transport/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +17 -4
- package/ios/MentraBluetoothSDK.podspec +3 -2
- package/ios/Source/BluetoothBackgroundMode.swift +32 -0
- package/ios/Source/BluetoothSdkDefaults.swift +7 -2
- package/ios/Source/Bridge.swift +15 -2
- package/ios/Source/DeviceManager.swift +102 -66
- package/ios/Source/DeviceStore.swift +26 -7
- package/ios/Source/GeneratedChangelogCatalog.swift +3 -2
- package/ios/Source/GeneratedReleaseMetadata.swift +6 -6
- package/ios/Source/MentraBluetoothSDK.swift +313 -54
- package/ios/Source/audio/GlassesMicWatchdog.swift +33 -0
- package/ios/Source/internal/BluetoothAvailability.swift +6 -0
- package/ios/Source/internal/BluetoothSdkAnalytics.swift +87 -13
- package/ios/Source/internal/BluetoothSdkAnalyticsTracker.swift +54 -1
- package/ios/Source/internal/BluetoothSdkInstallId.swift +21 -0
- package/ios/Source/internal/BluetoothSdkUpdateCheck.swift +223 -0
- package/ios/Source/internal/BluetoothSdkUsageTracker.swift +428 -0
- package/ios/Source/requests/DisplayRequests.swift +1 -1
- package/ios/Source/requests/VersionInfoResponseAccumulator.swift +11 -1
- package/ios/Source/sgcs/Ar99.swift +1 -2
- package/ios/Source/sgcs/G1.swift +68 -38
- package/ios/Source/sgcs/G1ConnectionTarget.swift +13 -0
- package/ios/Source/sgcs/G2.swift +261 -247
- package/ios/Source/sgcs/G2ConnectionSession.swift +101 -0
- package/ios/Source/sgcs/Mach1.swift +19 -7
- package/ios/Source/sgcs/MentraLive.swift +272 -197
- package/ios/Source/sgcs/MentraLiveFileTransferSession.swift +137 -0
- package/ios/Source/sgcs/MentraLivePairingAdvertisement.swift +47 -0
- package/ios/Source/sgcs/MentraNex.swift +8 -31
- package/ios/Source/sgcs/NexConnectionTarget.swift +15 -0
- package/ios/Source/sgcs/Nimo.swift +280 -408
- package/ios/Source/sgcs/NimoCanvasCodec.swift +272 -0
- package/ios/Source/sgcs/NimoCanvasCoordinator.swift +233 -0
- package/ios/Source/sgcs/NimoCanvasImage.swift +53 -0
- package/ios/Source/sgcs/NimoCanvasSession.swift +166 -0
- package/ios/Source/sgcs/NimoDiscovery.swift +49 -0
- package/ios/Source/sgcs/SGCManager.swift +25 -1
- package/ios/Source/sgcs/VuzixConnectionTarget.swift +15 -0
- package/ios/Source/status/DeviceStatus.swift +1 -0
- package/ios/Source/stt/SherpaOnnxTranscriber.swift +49 -189
- package/ios/Source/utils/AudioSessionMonitor.swift +44 -8
- package/ios/Source/utils/DemandDrivenAudioWorker.swift +150 -0
- package/ios/Source/utils/G1Text.swift +2 -24
- package/ios/Source/utils/G1TextSanitizer.swift +27 -0
- package/ios/Source/utils/G2Text.swift +11 -0
- package/ios/Source/utils/LatinTextSanitizer.swift +48 -0
- package/ios/Tests/BluetoothAudioRouteTests.swift +26 -0
- package/ios/Tests/BluetoothBackgroundModeTests.swift +32 -0
- package/ios/Tests/BluetoothSdkAnalyticsTrackerTests.swift +54 -3
- package/ios/Tests/BluetoothSdkUpdateCheckTests.swift +153 -0
- package/ios/Tests/BluetoothSdkUsageTrackerTests.swift +296 -0
- package/ios/Tests/DashboardContentFormatterTests.swift +2 -2
- package/ios/Tests/DashboardRenderingTests.swift +31 -0
- package/ios/Tests/DemandDrivenAudioWorkerTests.swift +157 -0
- package/ios/Tests/DeviceIdentityPairingTests.swift +49 -0
- package/ios/Tests/DeviceManagerReconnectTests.swift +362 -0
- package/ios/Tests/G1PairingTests.swift +162 -0
- package/ios/Tests/G1TextSanitizerTests.swift +32 -0
- package/ios/Tests/G2ConnectionSessionTests.swift +172 -0
- package/ios/Tests/G2TextTests.swift +10 -0
- package/ios/Tests/IncidentLogDispatchTests.swift +111 -0
- package/ios/Tests/MentraLiveFileTransferSessionTests.swift +47 -0
- package/ios/Tests/MentraLiveReconnectPolicyTests.swift +58 -0
- package/ios/Tests/MentraLiveSessionReadinessTests.swift +163 -0
- package/ios/Tests/NimoCanvasCodecTests.swift +217 -0
- package/ios/Tests/NimoCanvasSessionTests.swift +181 -0
- package/ios/Tests/NimoCanvasTransportTests.swift +186 -0
- package/ios/Tests/NimoDeviceCommandsTests.swift +16 -0
- package/ios/Tests/NimoDiscoveryTests.swift +79 -0
- package/ios/Tests/SGCConnectionTargetTests.swift +48 -0
- package/ios/Tests/VersionInfoCommandTests.swift +194 -0
- package/ios/Tests/VersionInfoResponseAccumulatorTests.swift +65 -0
- package/ios/Tests/VideoRecordingCommandTests.swift +81 -0
- package/ios/Tests/WifiCredentialLoggingTests.swift +42 -0
- package/package.json +2 -2
- package/plugin/build/withIos.d.ts +2 -0
- package/plugin/build/withIos.js +12 -1
- package/src/BluetoothSdk.types.ts +15 -0
- package/src/_private/BluetoothSdkModule.ts +10 -1
- package/src/_private/MentraLocalNetworkModule.ts +28 -0
- package/src/_private/nativeLogConsole.ts +19 -0
- package/src/generated/changelogCatalog.ts +6 -2
- package/src/generated/releaseMetadata.ts +6 -6
- package/src/index.ts +1 -0
- package/src/ota-transport/index.ts +10 -1
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.sgcs
|
|
2
|
+
|
|
3
|
+
/** Private FBP1 protocol for one pinned diagnostic build, never a general memory reader. */
|
|
4
|
+
internal object NimoFramebufferProtocol {
|
|
5
|
+
const val EXPECTED_VERSION = "FW-VERSION-v0.1.1.1-20260827164351-537cf1-dirty-Debug"
|
|
6
|
+
const val ARTIFACT_SHA256 = "4de441f887e85b1f8194881695b96c16e8018fbdc19d4edc9f5b5d4210e5aa83"
|
|
7
|
+
const val WIDTH = 540
|
|
8
|
+
const val HEIGHT = 280
|
|
9
|
+
const val PAGE_BYTES = 240
|
|
10
|
+
const val PAGE_COUNT = 315
|
|
11
|
+
const val BANK_BYTES = PAGE_BYTES * PAGE_COUNT
|
|
12
|
+
private val MAGIC = "FBP1".toByteArray(Charsets.US_ASCII)
|
|
13
|
+
private val BUILD_TAG = "537cf1p1".toByteArray(Charsets.US_ASCII)
|
|
14
|
+
|
|
15
|
+
data class Request(val nonce: Long, val op: Int, val bank: Int, val page: Int)
|
|
16
|
+
data class State(val draw: Int, val prepared: Int, val dirty: Int, val pending: Int)
|
|
17
|
+
data class Reply(val nonce: Long, val op: Int, val bank: Int, val page: Int,
|
|
18
|
+
val before: State, val after: State, val data: ByteArray)
|
|
19
|
+
|
|
20
|
+
fun hex(bytes: ByteArray): String {
|
|
21
|
+
val digits = "0123456789abcdef"
|
|
22
|
+
val chars = CharArray(bytes.size * 2)
|
|
23
|
+
bytes.forEachIndexed { index, byte ->
|
|
24
|
+
val value = byte.toInt() and 255
|
|
25
|
+
chars[index * 2] = digits[value ushr 4]
|
|
26
|
+
chars[index * 2 + 1] = digits[value and 15]
|
|
27
|
+
}
|
|
28
|
+
return chars.concatToString()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
fun queryFrame(key: Int): ByteArray {
|
|
32
|
+
require(key == 0x0B || key == 0x19) { "Only version and readiness queries are permitted" }
|
|
33
|
+
return frame(key, byteArrayOf())
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
fun requestFrame(request: Request): ByteArray {
|
|
37
|
+
validate(request)
|
|
38
|
+
val nonce = ByteArray(4) { (request.nonce shr (8 * it)).toByte() }
|
|
39
|
+
return frame(0x15, MAGIC + nonce + byteArrayOf(request.op.toByte(), request.bank.toByte(),
|
|
40
|
+
request.page.toByte(), (request.page shr 8).toByte()))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
fun version(packet: ByteArray): String {
|
|
44
|
+
val payload = basicPayload(packet, 0x0B)
|
|
45
|
+
require(payload.isNotEmpty() && payload[0] == 0.toByte()) { "Version query rejected" }
|
|
46
|
+
val expected = EXPECTED_VERSION.toByteArray(Charsets.US_ASCII)
|
|
47
|
+
require(payload.copyOfRange(1, payload.size).contentEquals(expected)) { "Unexpected firmware identity" }
|
|
48
|
+
return EXPECTED_VERSION
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Preserve status and all three readiness fields, including a not-ready result. */
|
|
52
|
+
fun readiness(packet: ByteArray): List<Int> {
|
|
53
|
+
val payload = basicPayload(packet, 0x19)
|
|
54
|
+
require(payload.size == 4) { "Readiness response must contain status, ready, TWS, and peer GATT" }
|
|
55
|
+
return payload.map { it.toInt() and 255 }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
fun basicPayload(packet: ByteArray, key: Int): ByteArray {
|
|
59
|
+
require(key == 0x0B || key == 0x19) { "Unexpected basic query" }
|
|
60
|
+
return payload(packet, key, 128)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
fun reply(packet: ByteArray, expected: Request): Reply {
|
|
64
|
+
validate(expected)
|
|
65
|
+
val body = payload(packet, 0x15, 23 + PAGE_BYTES)
|
|
66
|
+
require(body.size >= 23) { "Short FBP1 response" }
|
|
67
|
+
require(body.copyOfRange(1, 5).contentEquals(MAGIC)) { "FBP1 response magic mismatch" }
|
|
68
|
+
require(word(body, 21) == body.size - 23) { "FBP1 data length mismatch" }
|
|
69
|
+
val status = unsigned(body, 0)
|
|
70
|
+
require(status in listOf(0, 6, 7)) { "Unknown FBP1 response status $status" }
|
|
71
|
+
if (status != 0) {
|
|
72
|
+
require(body.size == 23) { "Rejected FBP1 response contains data" }
|
|
73
|
+
throw IllegalArgumentException("FBP1 request rejected with status $status")
|
|
74
|
+
}
|
|
75
|
+
val nonce = (0..3).fold(0L) { value, i -> value or (unsigned(body, 5 + i).toLong() shl (8 * i)) }
|
|
76
|
+
val actual = Request(nonce, unsigned(body, 9), unsigned(body, 10), word(body, 11))
|
|
77
|
+
require(actual == expected) { "FBP1 response correlation mismatch" }
|
|
78
|
+
val before = state(body, 13)
|
|
79
|
+
val after = state(body, 17)
|
|
80
|
+
require(before == after) { "Framebuffer metadata changed during page copy" }
|
|
81
|
+
val data = body.copyOfRange(23, body.size)
|
|
82
|
+
if (actual.op == 0) require(data.contentEquals(BUILD_TAG)) { "Unexpected FBP1 build tag" }
|
|
83
|
+
else require(data.size == PAGE_BYTES) { "Framebuffer page must contain $PAGE_BYTES bytes" }
|
|
84
|
+
return Reply(nonce, actual.op, actual.bank, actual.page, before, after, data)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Sequential, single-pass assembler. Any rejected input permanently invalidates the read. */
|
|
88
|
+
class BankRead(val nonce: Long, val bank: Int) {
|
|
89
|
+
var pageCount = 0
|
|
90
|
+
private set
|
|
91
|
+
var metadata: State? = null
|
|
92
|
+
private set
|
|
93
|
+
private var failed = false
|
|
94
|
+
private val data = ByteArray(BANK_BYTES)
|
|
95
|
+
|
|
96
|
+
init { validate(Request(nonce, 1, bank, 0)) }
|
|
97
|
+
|
|
98
|
+
fun add(reply: Reply) {
|
|
99
|
+
require(!failed) { "Bank read was invalidated" }
|
|
100
|
+
try {
|
|
101
|
+
require(pageCount < PAGE_COUNT && reply.page == pageCount) { "Duplicate or out-of-order framebuffer page" }
|
|
102
|
+
require(reply.nonce == nonce && reply.bank == bank && reply.op == 1) { "Bank read correlation mismatch" }
|
|
103
|
+
require(reply.data.size == PAGE_BYTES) { "Invalid framebuffer page length" }
|
|
104
|
+
require(reply.before == reply.after) { "Metadata changed during page copy" }
|
|
105
|
+
require(metadata == null || metadata == reply.before) { "Metadata changed between pages" }
|
|
106
|
+
metadata = reply.before
|
|
107
|
+
reply.data.copyInto(data, pageCount * PAGE_BYTES)
|
|
108
|
+
pageCount += 1
|
|
109
|
+
} catch (error: IllegalArgumentException) {
|
|
110
|
+
failed = true
|
|
111
|
+
throw error
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
fun bytes(): ByteArray {
|
|
116
|
+
require(!failed && pageCount == PAGE_COUNT) { "Framebuffer bank is incomplete or invalidated" }
|
|
117
|
+
return data.copyOf()
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
fun verifyPair(first: BankRead, second: BankRead) {
|
|
122
|
+
require(first.nonce != second.nonce) { "Static verification requires independent nonces" }
|
|
123
|
+
require(first.bank == second.bank && first.metadata == second.metadata) { "Static pair metadata mismatch" }
|
|
124
|
+
require(first.bytes().contentEquals(second.bytes())) { "Framebuffer pixels changed between passes" }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private fun validate(request: Request) {
|
|
128
|
+
require(request.nonce in 0..0xFFFFFFFFL && request.op in 0..1 && request.bank in 0..1 &&
|
|
129
|
+
request.page in 0 until PAGE_COUNT && (request.op != 0 || request.page == 0)) { "Invalid FBP1 request" }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private fun state(body: ByteArray, offset: Int): State {
|
|
133
|
+
val state = State(unsigned(body, offset), unsigned(body, offset + 1), unsigned(body, offset + 2), unsigned(body, offset + 3))
|
|
134
|
+
require(state.draw in 0..1 && state.prepared in 0..1) { "Invalid framebuffer selectors" }
|
|
135
|
+
return state
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private fun payload(packet: ByteArray, key: Int, maximum: Int): ByteArray {
|
|
139
|
+
require(packet.size in 12..(12 + maximum)) { "Invalid frame size" }
|
|
140
|
+
require(unsigned(packet, 0) == 0xBF && packet[1] == 0.toByte()) { "Expected BF00 frame" }
|
|
141
|
+
require(word(packet, 6) == 0) { "Fragmented framebuffer responses are not supported" }
|
|
142
|
+
require(word(packet, 2) == packet.size - 8 && word(packet, 10) == packet.size - 12) { "Frame length mismatch" }
|
|
143
|
+
require(unsigned(packet, 8) == 2 && unsigned(packet, 9) == key) { "Frame command mismatch" }
|
|
144
|
+
require(word(packet, 4) == NimoCanvasCodec.crc16(packet.copyOfRange(8, packet.size))) { "Frame CRC mismatch" }
|
|
145
|
+
return packet.copyOfRange(12, packet.size)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private fun frame(key: Int, payload: ByteArray): ByteArray {
|
|
149
|
+
val body = byteArrayOf(2, key.toByte(), payload.size.toByte(), (payload.size shr 8).toByte()) + payload
|
|
150
|
+
val crc = NimoCanvasCodec.crc16(body)
|
|
151
|
+
return byteArrayOf(0xBF.toByte(), 0, body.size.toByte(), (body.size shr 8).toByte(),
|
|
152
|
+
crc.toByte(), (crc shr 8).toByte(), 0, 0) + body
|
|
153
|
+
}
|
|
154
|
+
private fun unsigned(bytes: ByteArray, index: Int) = bytes[index].toInt() and 255
|
|
155
|
+
private fun word(bytes: ByteArray, index: Int) = unsigned(bytes, index) or (unsigned(bytes, index + 1) shl 8)
|
|
156
|
+
}
|
|
@@ -46,7 +46,12 @@ internal class BluetoothSdkAnalytics(
|
|
|
46
46
|
// The tracker is touched from store listeners and SDK entry points, hence
|
|
47
47
|
// @Synchronized on the methods that read or write it.
|
|
48
48
|
private val tracker = BluetoothSdkAnalyticsTracker(DeviceTypes.SIMULATED)
|
|
49
|
+
private val usage = BluetoothSdkUsageTracker()
|
|
49
50
|
private var startedCaptured = false
|
|
51
|
+
private var lastGlasses: AnalyticsGlassesSnapshot? = null
|
|
52
|
+
// Set by an SDK disconnect/forget/cancel call and consumed by the next
|
|
53
|
+
// connected -> disconnected transition, so that transition can say why.
|
|
54
|
+
private var pendingDisconnectReason: String? = null
|
|
50
55
|
// Resolved once, lazily, on the transport executor: PackageManager lookups
|
|
51
56
|
// must not run on the caller (often main or the Bluetooth status) thread.
|
|
52
57
|
private val hostProperties: Map<String, Any> by lazy { BluetoothSdkAnalyticsHost.resolve(appContext).toMap() }
|
|
@@ -56,9 +61,15 @@ internal class BluetoothSdkAnalytics(
|
|
|
56
61
|
timeZone = TimeZone.getTimeZone("UTC")
|
|
57
62
|
}
|
|
58
63
|
|
|
64
|
+
/** False when the host disabled analytics; callers skip building usage facts entirely. */
|
|
65
|
+
val isEnabled: Boolean
|
|
66
|
+
get() = config.isReady
|
|
67
|
+
|
|
59
68
|
@Synchronized
|
|
60
69
|
fun initializeGlassesStatus(status: GlassesStatus) {
|
|
61
|
-
|
|
70
|
+
val snapshot = status.toAnalyticsSnapshot()
|
|
71
|
+
lastGlasses = snapshot
|
|
72
|
+
tracker.initialize(snapshot, BluetoothSdkAnalyticsTracker.reportingDay(System.currentTimeMillis()))
|
|
62
73
|
}
|
|
63
74
|
|
|
64
75
|
@Synchronized
|
|
@@ -71,12 +82,54 @@ internal class BluetoothSdkAnalytics(
|
|
|
71
82
|
}
|
|
72
83
|
|
|
73
84
|
@Synchronized
|
|
74
|
-
fun observeGlassesStatus(status: GlassesStatus) {
|
|
75
|
-
val
|
|
85
|
+
fun observeGlassesStatus(status: GlassesStatus, phoneBluetoothOff: () -> Boolean) {
|
|
86
|
+
val snapshot = status.toAnalyticsSnapshot()
|
|
87
|
+
lastGlasses = snapshot
|
|
88
|
+
val now = System.currentTimeMillis()
|
|
89
|
+
val events =
|
|
90
|
+
tracker.observe(snapshot, BluetoothSdkAnalyticsTracker.reportingDay(now), now) {
|
|
91
|
+
pendingDisconnectReason
|
|
92
|
+
?: if (phoneBluetoothOff()) {
|
|
93
|
+
BluetoothSdkAnalyticsTracker.DISCONNECT_BLUETOOTH_OFF
|
|
94
|
+
} else {
|
|
95
|
+
BluetoothSdkAnalyticsTracker.DISCONNECT_LINK_LOST
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (!snapshot.connected || events.any { it.name == "bluetooth_sdk_glasses_connected" }) {
|
|
99
|
+
pendingDisconnectReason = null
|
|
100
|
+
}
|
|
76
101
|
if (!config.isReady) return
|
|
77
102
|
for (event in events) capture(event.name, event.properties)
|
|
78
103
|
}
|
|
79
104
|
|
|
105
|
+
@Synchronized
|
|
106
|
+
fun noteDisconnectRequested(reason: String) {
|
|
107
|
+
if (lastGlasses?.connected == true) pendingDisconnectReason = reason
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@Synchronized
|
|
111
|
+
fun recordUsage(build: BluetoothSdkUsageTracker.() -> AnalyticsEvent?) {
|
|
112
|
+
if (!config.isReady) return
|
|
113
|
+
val event = usage.build() ?: return
|
|
114
|
+
capture(event.name, glassesContext() + event.properties)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Usage-tracker state changes that emit nothing themselves (e.g. a stop request). */
|
|
118
|
+
@Synchronized
|
|
119
|
+
fun updateUsage(update: BluetoothSdkUsageTracker.() -> Unit) {
|
|
120
|
+
if (!config.isReady) return
|
|
121
|
+
usage.update()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private fun glassesContext(): Map<String, Any> {
|
|
125
|
+
val glasses = lastGlasses?.takeIf { it.connected } ?: return emptyMap()
|
|
126
|
+
return buildMap {
|
|
127
|
+
glasses.model.takeIf { it.isNotBlank() }?.let { put("glasses_model", it) }
|
|
128
|
+
glasses.serialNumber.validManufacturingSerial()?.let { put("glasses_device_id", it) }
|
|
129
|
+
glasses.buildNumber.takeIf { it.isNotBlank() }?.let { put("glasses_build_number", it) }
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
80
133
|
/**
|
|
81
134
|
* Delivery is owned by the process-wide transport, so nothing is torn down
|
|
82
135
|
* here: a retry in flight when this instance closes still completes, and a
|
|
@@ -101,7 +154,7 @@ internal class BluetoothSdkAnalytics(
|
|
|
101
154
|
"api_key" to DEFAULT_POSTHOG_API_KEY,
|
|
102
155
|
"uuid" to uuid,
|
|
103
156
|
"event" to eventName,
|
|
104
|
-
"distinct_id" to
|
|
157
|
+
"distinct_id" to BluetoothSdkInstallId.get(appContext),
|
|
105
158
|
"timestamp" to isoFormat.format(Date(capturedAt)),
|
|
106
159
|
"properties" to baseProperties(activeConfig) + hostProperties + eventProperties,
|
|
107
160
|
)
|
|
@@ -156,16 +209,12 @@ internal class BluetoothSdkAnalytics(
|
|
|
156
209
|
put("app_package", appContext.packageName)
|
|
157
210
|
put("os_platform", "android")
|
|
158
211
|
put("os_version", Build.VERSION.SDK_INT)
|
|
212
|
+
Build.MANUFACTURER?.takeIf { it.isNotBlank() }?.let { put("phone_manufacturer", it) }
|
|
213
|
+
Build.MODEL?.takeIf { it.isNotBlank() }?.let { put("phone_model", it) }
|
|
214
|
+
put("locale", Locale.getDefault().toLanguageTag())
|
|
215
|
+
put("timezone", TimeZone.getDefault().id)
|
|
159
216
|
}
|
|
160
217
|
|
|
161
|
-
private fun distinctId(): String {
|
|
162
|
-
val prefs = appContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
163
|
-
prefs.getString(PREFS_DISTINCT_ID, null)?.let { return it }
|
|
164
|
-
val generated = "mentra-bt-sdk-${UUID.randomUUID()}"
|
|
165
|
-
prefs.edit().putString(PREFS_DISTINCT_ID, generated).apply()
|
|
166
|
-
return generated
|
|
167
|
-
}
|
|
168
|
-
|
|
169
218
|
private fun captureUrl(): String {
|
|
170
219
|
val normalized = DEFAULT_POSTHOG_HOST.trim().trimEnd('/')
|
|
171
220
|
return "$normalized/i/v0/e/"
|
|
@@ -173,8 +222,6 @@ internal class BluetoothSdkAnalytics(
|
|
|
173
222
|
|
|
174
223
|
companion object {
|
|
175
224
|
internal const val META_ANALYTICS_DISABLED = "com.mentra.bluetoothsdk.analytics.disabled"
|
|
176
|
-
private const val PREFS_NAME = "mentra_bluetooth_sdk_analytics"
|
|
177
|
-
private const val PREFS_DISTINCT_ID = "distinct_id"
|
|
178
225
|
private const val DEFAULT_POSTHOG_API_KEY = "phc_FCweXVAxVgU7wZK4Fk3okOx4RmyNqVHJf62YpZSfJt5"
|
|
179
226
|
private const val DEFAULT_POSTHOG_HOST = "https://us.i.posthog.com"
|
|
180
227
|
}
|
|
@@ -212,4 +259,6 @@ private fun GlassesStatus.toAnalyticsSnapshot(): AnalyticsGlassesSnapshot =
|
|
|
212
259
|
androidVersion = androidVersion,
|
|
213
260
|
appVersion = appVersion,
|
|
214
261
|
buildNumber = buildNumber,
|
|
262
|
+
batteryLevel = batteryLevel,
|
|
263
|
+
charging = charging,
|
|
215
264
|
)
|
|
@@ -14,6 +14,9 @@ internal data class AnalyticsGlassesSnapshot(
|
|
|
14
14
|
val androidVersion: String = "",
|
|
15
15
|
val appVersion: String = "",
|
|
16
16
|
val buildNumber: String = "",
|
|
17
|
+
/** -1 while the glasses have not reported a level. */
|
|
18
|
+
val batteryLevel: Int = -1,
|
|
19
|
+
val charging: Boolean = false,
|
|
17
20
|
)
|
|
18
21
|
|
|
19
22
|
internal data class AnalyticsEvent(
|
|
@@ -36,12 +39,21 @@ internal data class AnalyticsEvent(
|
|
|
36
39
|
* is visible in both weeks. Reporting days follow `America/Los_Angeles`, the
|
|
37
40
|
* calendar the WAU weeks are cut on; a UTC day would miss the Sunday-evening to
|
|
38
41
|
* Monday-morning Pacific boundary, which is one UTC day.
|
|
42
|
+
* - `bluetooth_sdk_glasses_disconnected` when a counted connection ends, with its
|
|
43
|
+
* length, why it ended, and the battery levels seen during it. The reason is
|
|
44
|
+
* supplied by the caller: an SDK disconnect/forget/cancel call, the phone's
|
|
45
|
+
* Bluetooth turning off, or otherwise a lost link.
|
|
39
46
|
*/
|
|
40
47
|
internal class BluetoothSdkAnalyticsTracker(private val simulatedModel: String) {
|
|
41
48
|
private var lastConnected = false
|
|
42
49
|
private var connectedPendingModel = false
|
|
43
50
|
private var identifiedSerial: String? = null
|
|
44
51
|
private var identifiedReportingDay: Long? = null
|
|
52
|
+
private var connectedAtMs: Long? = null
|
|
53
|
+
private var batteryAtConnect = -1
|
|
54
|
+
private var lastBattery = -1
|
|
55
|
+
private var lastCharging: Boolean? = null
|
|
56
|
+
private var lastModel = ""
|
|
45
57
|
|
|
46
58
|
fun initialize(snapshot: AnalyticsGlassesSnapshot, reportingDay: Long) {
|
|
47
59
|
lastConnected = snapshot.connected
|
|
@@ -54,7 +66,12 @@ internal class BluetoothSdkAnalyticsTracker(private val simulatedModel: String)
|
|
|
54
66
|
identifiedReportingDay = identifiedSerial?.let { reportingDay }
|
|
55
67
|
}
|
|
56
68
|
|
|
57
|
-
fun observe(
|
|
69
|
+
fun observe(
|
|
70
|
+
snapshot: AnalyticsGlassesSnapshot,
|
|
71
|
+
reportingDay: Long,
|
|
72
|
+
nowMs: Long = 0L,
|
|
73
|
+
disconnectReason: () -> String = { DISCONNECT_UNKNOWN },
|
|
74
|
+
): List<AnalyticsEvent> {
|
|
58
75
|
val events = mutableListOf<AnalyticsEvent>()
|
|
59
76
|
val wasConnected = lastConnected
|
|
60
77
|
lastConnected = snapshot.connected
|
|
@@ -64,14 +81,30 @@ internal class BluetoothSdkAnalyticsTracker(private val simulatedModel: String)
|
|
|
64
81
|
connectedPendingModel = false
|
|
65
82
|
events += connectedEvent(snapshot, modelUnresolved = true)
|
|
66
83
|
}
|
|
84
|
+
connectedAtMs?.let { startedAt ->
|
|
85
|
+
events += disconnectedEvent(nowMs - startedAt, disconnectReason())
|
|
86
|
+
}
|
|
87
|
+
connectedAtMs = null
|
|
67
88
|
identifiedSerial = null
|
|
68
89
|
identifiedReportingDay = null
|
|
69
90
|
return events
|
|
70
91
|
}
|
|
71
92
|
|
|
93
|
+
if (snapshot.model.isNotBlank()) lastModel = snapshot.model
|
|
94
|
+
if (snapshot.batteryLevel >= 0) {
|
|
95
|
+
if (batteryAtConnect < 0) batteryAtConnect = snapshot.batteryLevel
|
|
96
|
+
lastBattery = snapshot.batteryLevel
|
|
97
|
+
lastCharging = snapshot.charging
|
|
98
|
+
}
|
|
99
|
+
|
|
72
100
|
if (!wasConnected) {
|
|
73
101
|
identifiedSerial = null
|
|
74
102
|
identifiedReportingDay = null
|
|
103
|
+
connectedAtMs = nowMs
|
|
104
|
+
lastModel = snapshot.model
|
|
105
|
+
batteryAtConnect = snapshot.batteryLevel
|
|
106
|
+
lastBattery = snapshot.batteryLevel
|
|
107
|
+
lastCharging = snapshot.charging.takeIf { snapshot.batteryLevel >= 0 }
|
|
75
108
|
if (snapshot.model.isBlank()) {
|
|
76
109
|
connectedPendingModel = true
|
|
77
110
|
} else {
|
|
@@ -106,6 +139,21 @@ internal class BluetoothSdkAnalyticsTracker(private val simulatedModel: String)
|
|
|
106
139
|
},
|
|
107
140
|
)
|
|
108
141
|
|
|
142
|
+
private fun disconnectedEvent(durationMs: Long, reason: String) =
|
|
143
|
+
AnalyticsEvent(
|
|
144
|
+
"bluetooth_sdk_glasses_disconnected",
|
|
145
|
+
buildMap {
|
|
146
|
+
put("event_kind", "glasses_disconnected")
|
|
147
|
+
put("session_duration_ms", durationMs.coerceAtLeast(0))
|
|
148
|
+
put("disconnect_reason", reason)
|
|
149
|
+
lastModel.takeIf { it.isNotBlank() }?.let { put("glasses_model", it) }
|
|
150
|
+
put("glasses_is_simulated", lastModel == simulatedModel)
|
|
151
|
+
batteryAtConnect.takeIf { it >= 0 }?.let { put("battery_level_at_connect", it) }
|
|
152
|
+
lastBattery.takeIf { it >= 0 }?.let { put("battery_level_at_disconnect", it) }
|
|
153
|
+
lastCharging?.let { put("charging", it) }
|
|
154
|
+
},
|
|
155
|
+
)
|
|
156
|
+
|
|
109
157
|
private fun identifiedEvent(snapshot: AnalyticsGlassesSnapshot, serial: String, kind: String) =
|
|
110
158
|
AnalyticsEvent(
|
|
111
159
|
"bluetooth_sdk_glasses_identified",
|
|
@@ -126,6 +174,12 @@ internal class BluetoothSdkAnalyticsTracker(private val simulatedModel: String)
|
|
|
126
174
|
)
|
|
127
175
|
|
|
128
176
|
companion object {
|
|
177
|
+
const val DISCONNECT_USER = "user_disconnect"
|
|
178
|
+
const val DISCONNECT_FORGET = "forget"
|
|
179
|
+
const val DISCONNECT_CANCEL = "cancel"
|
|
180
|
+
const val DISCONNECT_BLUETOOTH_OFF = "bluetooth_off"
|
|
181
|
+
const val DISCONNECT_LINK_LOST = "link_lost"
|
|
182
|
+
const val DISCONNECT_UNKNOWN = "unknown"
|
|
129
183
|
const val MILLIS_PER_DAY = 86_400_000L
|
|
130
184
|
val REPORTING_ZONE: TimeZone = TimeZone.getTimeZone("America/Los_Angeles")
|
|
131
185
|
|
|
@@ -3,5 +3,8 @@ package com.mentra.bluetoothsdk
|
|
|
3
3
|
/** Defaults for the public Bluetooth SDK surface. */
|
|
4
4
|
object BluetoothSdkDefaults {
|
|
5
5
|
const val VOICE_ACTIVITY_DETECTION_ENABLED = false
|
|
6
|
-
const val LOUDNESS_GATE_ENABLED =
|
|
6
|
+
const val LOUDNESS_GATE_ENABLED = true
|
|
7
|
+
|
|
8
|
+
/** Off until someone turns it on in Super Mode. Re-pushed on connect. */
|
|
9
|
+
const val AUTO_POWER_OFF_ENABLED = false
|
|
7
10
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import java.util.UUID
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* One random id per app install, shared by usage analytics (as the PostHog
|
|
8
|
+
* `distinct_id`) and the startup update check, so both describe the same install.
|
|
9
|
+
* It is created on first use whether or not analytics is enabled.
|
|
10
|
+
*/
|
|
11
|
+
internal object BluetoothSdkInstallId {
|
|
12
|
+
private const val PREFS_NAME = "mentra_bluetooth_sdk_analytics"
|
|
13
|
+
private const val PREFS_DISTINCT_ID = "distinct_id"
|
|
14
|
+
|
|
15
|
+
@Synchronized
|
|
16
|
+
fun get(context: Context): String {
|
|
17
|
+
val prefs = context.applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
18
|
+
prefs.getString(PREFS_DISTINCT_ID, null)?.let { return it }
|
|
19
|
+
val generated = "mentra-bt-sdk-${UUID.randomUUID()}"
|
|
20
|
+
prefs.edit().putString(PREFS_DISTINCT_ID, generated).apply()
|
|
21
|
+
return generated
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -334,6 +334,7 @@ class BluetoothSdkModule : Module() {
|
|
|
334
334
|
"glasses_status",
|
|
335
335
|
"bluetooth_status",
|
|
336
336
|
"log",
|
|
337
|
+
"sdk_update_notice",
|
|
337
338
|
"device_discovered",
|
|
338
339
|
"default_device_changed",
|
|
339
340
|
// Individual event handlers
|
|
@@ -404,7 +405,6 @@ class BluetoothSdkModule : Module() {
|
|
|
404
405
|
"send_command_to_ble",
|
|
405
406
|
"receive_command_from_ble",
|
|
406
407
|
"miniapp_selected",
|
|
407
|
-
"captions_tester_incident",
|
|
408
408
|
"extraction_progress",
|
|
409
409
|
)
|
|
410
410
|
|
|
@@ -550,6 +550,8 @@ class BluetoothSdkModule : Module() {
|
|
|
550
550
|
|
|
551
551
|
SdkAsyncFunction("forget") { -> sdk?.forget() }
|
|
552
552
|
|
|
553
|
+
SdkCoroutineFunction("unpair") { -> deviceManager?.unpair() }
|
|
554
|
+
|
|
553
555
|
AsyncFunction("connectDefaultController") { deviceManager?.connectDefaultController() }
|
|
554
556
|
|
|
555
557
|
AsyncFunction("disconnectController") { deviceManager?.disconnectController() }
|
|
@@ -616,7 +618,7 @@ class BluetoothSdkModule : Module() {
|
|
|
616
618
|
// MARK: - Incident Reporting
|
|
617
619
|
|
|
618
620
|
SdkAsyncFunction("sendIncidentId") { incidentId: String, apiBaseUrl: String? ->
|
|
619
|
-
|
|
621
|
+
requireSdk().sendIncidentId(incidentId, apiBaseUrl)
|
|
620
622
|
}
|
|
621
623
|
|
|
622
624
|
// MARK: - Native Notification Centre
|
|
@@ -676,6 +678,10 @@ class BluetoothSdkModule : Module() {
|
|
|
676
678
|
sdk?.setLoudnessGateEnabled(enabled)
|
|
677
679
|
}
|
|
678
680
|
|
|
681
|
+
SdkAsyncFunction("setAutoPowerOffEnabled") { enabled: Boolean ->
|
|
682
|
+
sdk?.setAutoPowerOffEnabled(enabled)
|
|
683
|
+
}
|
|
684
|
+
|
|
679
685
|
@Suppress("DEPRECATION")
|
|
680
686
|
SdkCoroutineFunction("setPhotoCaptureDefaults") { params: Map<String, Any?> ->
|
|
681
687
|
requireSdk().setPhotoCaptureDefaults(PhotoCaptureDefaults.fromMap(params)).values
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.os.Build
|
|
5
|
+
import org.json.JSONObject
|
|
6
|
+
import java.net.HttpURLConnection
|
|
7
|
+
import java.net.URL
|
|
8
|
+
import java.util.concurrent.Executors
|
|
9
|
+
import java.util.concurrent.RejectedExecutionException
|
|
10
|
+
import java.util.concurrent.ScheduledExecutorService
|
|
11
|
+
import java.util.concurrent.ScheduledFuture
|
|
12
|
+
import java.util.concurrent.TimeUnit
|
|
13
|
+
|
|
14
|
+
internal data class BluetoothSdkUpdateCheckResult(
|
|
15
|
+
val status: String,
|
|
16
|
+
val channel: String,
|
|
17
|
+
val latestVersion: String?,
|
|
18
|
+
val minimumSupportedVersion: String?,
|
|
19
|
+
) {
|
|
20
|
+
companion object {
|
|
21
|
+
fun parse(body: String): BluetoothSdkUpdateCheckResult? =
|
|
22
|
+
try {
|
|
23
|
+
val json = JSONObject(body)
|
|
24
|
+
BluetoothSdkUpdateCheckResult(
|
|
25
|
+
status = json.optString("status"),
|
|
26
|
+
channel = json.optString("channel"),
|
|
27
|
+
latestVersion = json.optNonBlankString("latestVersion"),
|
|
28
|
+
minimumSupportedVersion = json.optNonBlankString("minimumSupportedVersion"),
|
|
29
|
+
)
|
|
30
|
+
} catch (_: Exception) {
|
|
31
|
+
null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private fun JSONObject.optNonBlankString(key: String): String? =
|
|
35
|
+
if (isNull(key)) null else optString(key).trim().takeIf { it.isNotEmpty() }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The developer-facing console banner for an out-of-date SDK. Never shown in app UI. */
|
|
40
|
+
internal object BluetoothSdkUpdateNotice {
|
|
41
|
+
const val STATUS_UPDATE_AVAILABLE = "update_available"
|
|
42
|
+
const val STATUS_UNSUPPORTED = "unsupported"
|
|
43
|
+
private const val RULE = "=================================================================="
|
|
44
|
+
|
|
45
|
+
/** Null when the installed SDK is current or the server could not tell. */
|
|
46
|
+
fun banner(installedVersion: String, result: BluetoothSdkUpdateCheckResult, surface: String): String? {
|
|
47
|
+
val latest = result.latestVersion
|
|
48
|
+
val heading =
|
|
49
|
+
when (result.status) {
|
|
50
|
+
STATUS_UNSUPPORTED -> "MENTRA BLUETOOTH SDK $installedVersion IS NO LONGER SUPPORTED"
|
|
51
|
+
STATUS_UPDATE_AVAILABLE -> if (latest != null) "MENTRA BLUETOOTH SDK UPDATE AVAILABLE" else return null
|
|
52
|
+
else -> return null
|
|
53
|
+
}
|
|
54
|
+
return buildString {
|
|
55
|
+
appendLine(RULE)
|
|
56
|
+
appendLine(" $heading")
|
|
57
|
+
appendLine(" Installed: $installedVersion Latest: ${latest ?: "unknown"}")
|
|
58
|
+
appendLine(" Update: ${updateLine(surface, latest)}")
|
|
59
|
+
if (result.status == STATUS_UNSUPPORTED) {
|
|
60
|
+
appendLine(" This version has known issues that are fixed in newer releases.")
|
|
61
|
+
}
|
|
62
|
+
appendLine(" Running an old SDK is the most common cause of glasses issues.")
|
|
63
|
+
append(RULE)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private fun updateLine(surface: String, version: String?): String =
|
|
68
|
+
when (surface) {
|
|
69
|
+
"react_native" -> "bun add @mentra/bluetooth-sdk@${version ?: "latest"}"
|
|
70
|
+
"ios" -> "the MentraBluetoothSDK Swift package at ${version ?: "its latest release"}"
|
|
71
|
+
else -> "com.mentraglass:bluetooth-sdk:${version ?: "<latest release>"}"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Checks Mentra Core for a newer SDK on every SDK start and, when this SDK is
|
|
77
|
+
* behind, prints a console banner now and every [REPRINT_INTERVAL_MINUTES] while
|
|
78
|
+
* the SDK instance lives. The check is one request with a short timeout and no
|
|
79
|
+
* retry; any failure is silent and changes no SDK behavior. It runs regardless
|
|
80
|
+
* of the analytics setting.
|
|
81
|
+
*/
|
|
82
|
+
internal class BluetoothSdkUpdateCheck(
|
|
83
|
+
context: Context,
|
|
84
|
+
private val surface: String,
|
|
85
|
+
private val sdkVersion: String = BuildConfig.SDK_VERSION,
|
|
86
|
+
private val transport: (String) -> String? = ::post,
|
|
87
|
+
private val onNotice: (banner: String, result: BluetoothSdkUpdateCheckResult) -> Unit,
|
|
88
|
+
) {
|
|
89
|
+
private val appContext = context.applicationContext
|
|
90
|
+
private val lock = Any()
|
|
91
|
+
private var stopped = false
|
|
92
|
+
private var reprint: ScheduledFuture<*>? = null
|
|
93
|
+
|
|
94
|
+
fun start() {
|
|
95
|
+
if (!isCheckable(sdkVersion)) return
|
|
96
|
+
try {
|
|
97
|
+
executor.execute { runCatching { check() } }
|
|
98
|
+
} catch (_: RejectedExecutionException) {
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
fun stop() {
|
|
103
|
+
synchronized(lock) {
|
|
104
|
+
stopped = true
|
|
105
|
+
reprint?.cancel(false)
|
|
106
|
+
reprint = null
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private fun check() {
|
|
111
|
+
val response = transport(requestBody().toString()) ?: return
|
|
112
|
+
val result = BluetoothSdkUpdateCheckResult.parse(response) ?: return
|
|
113
|
+
val banner = BluetoothSdkUpdateNotice.banner(sdkVersion, result, surface) ?: return
|
|
114
|
+
synchronized(lock) {
|
|
115
|
+
if (stopped) return
|
|
116
|
+
onNotice(banner, result)
|
|
117
|
+
reprint =
|
|
118
|
+
executor.scheduleAtFixedRate(
|
|
119
|
+
{ synchronized(lock) { if (!stopped) onNotice(banner, result) } },
|
|
120
|
+
REPRINT_INTERVAL_MINUTES,
|
|
121
|
+
REPRINT_INTERVAL_MINUTES,
|
|
122
|
+
TimeUnit.MINUTES,
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
internal fun requestBody(): JSONObject {
|
|
128
|
+
val host = BluetoothSdkAnalyticsHost.resolve(appContext)
|
|
129
|
+
return JSONObject().apply {
|
|
130
|
+
put("install_id", BluetoothSdkInstallId.get(appContext))
|
|
131
|
+
put("app_identifier", appContext.packageName)
|
|
132
|
+
host.appVersion?.let { put("app_version", it) }
|
|
133
|
+
host.appBuild?.let { put("app_build", it) }
|
|
134
|
+
put("app_build_type", host.buildType)
|
|
135
|
+
put("app_install_source", host.installSource)
|
|
136
|
+
host.environment?.let { put("app_environment", it) }
|
|
137
|
+
put("sdk_version", sdkVersion)
|
|
138
|
+
put("sdk_platform", "android")
|
|
139
|
+
put("sdk_surface", surface)
|
|
140
|
+
put("os_platform", "android")
|
|
141
|
+
put("os_version", Build.VERSION.SDK_INT.toString())
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
companion object {
|
|
146
|
+
const val UPDATE_CHECK_URL = "https://core.mentraglass.com/api/client/bluetooth-sdk/update-check"
|
|
147
|
+
const val REPRINT_INTERVAL_MINUTES = 15L
|
|
148
|
+
private const val TIMEOUT_MS = 4_000
|
|
149
|
+
|
|
150
|
+
private val executor: ScheduledExecutorService =
|
|
151
|
+
Executors.newSingleThreadScheduledExecutor { runnable ->
|
|
152
|
+
Thread(runnable, "MentraBluetoothSdkUpdateCheck").apply { isDaemon = true }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
fun isCheckable(version: String): Boolean = version.isNotBlank() && version != "unknown"
|
|
156
|
+
|
|
157
|
+
private fun post(body: String): String? {
|
|
158
|
+
val connection = URL(UPDATE_CHECK_URL).openConnection() as HttpURLConnection
|
|
159
|
+
return try {
|
|
160
|
+
connection.requestMethod = "POST"
|
|
161
|
+
connection.connectTimeout = TIMEOUT_MS
|
|
162
|
+
connection.readTimeout = TIMEOUT_MS
|
|
163
|
+
connection.doOutput = true
|
|
164
|
+
connection.setRequestProperty("Content-Type", "application/json")
|
|
165
|
+
connection.outputStream.use { it.write(body.toByteArray(Charsets.UTF_8)) }
|
|
166
|
+
if (connection.responseCode != 200) {
|
|
167
|
+
connection.errorStream?.close()
|
|
168
|
+
null
|
|
169
|
+
} else {
|
|
170
|
+
connection.inputStream.bufferedReader(Charsets.UTF_8).use { it.readText() }
|
|
171
|
+
}
|
|
172
|
+
} catch (_: Exception) {
|
|
173
|
+
null
|
|
174
|
+
} finally {
|
|
175
|
+
connection.disconnect()
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Test hook: waits until every task submitted so far has run. */
|
|
180
|
+
internal fun awaitIdle() {
|
|
181
|
+
executor.submit {}.get()
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|