@mentra/bluetooth-sdk 3.2.1-dev.314 → 3.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- 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/BluetoothSdkDefaults.kt +3 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +6 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +2 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +129 -25
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +37 -13
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +2 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +4 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/VersionInfoResponseAccumulator.kt +13 -2
- 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/G1.kt +2 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +72 -21
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +101 -22
- 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 +245 -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/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/VersionInfoResponseAccumulatorTest.kt +71 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/services/G2ConnectionRecoveryTest.kt +51 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/G1PairingTest.kt +105 -0
- 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/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/MentraLivePairingTargetTest.kt +65 -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 +220 -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 +13 -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 +3 -0
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/generated/changelogCatalog.d.ts +2 -2
- package/build/generated/changelogCatalog.js +2 -2
- package/build/generated/changelogCatalog.js.map +1 -1
- package/build/generated/releaseMetadata.js +5 -5
- 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/ios/BluetoothSdkModule.swift +9 -0
- package/ios/MentraBluetoothSDK.podspec +3 -2
- package/ios/Source/BluetoothBackgroundMode.swift +32 -0
- package/ios/Source/BluetoothSdkDefaults.swift +6 -1
- package/ios/Source/Bridge.swift +15 -2
- package/ios/Source/DeviceManager.swift +96 -63
- package/ios/Source/DeviceStore.swift +26 -7
- package/ios/Source/GeneratedChangelogCatalog.swift +2 -2
- package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
- package/ios/Source/MentraBluetoothSDK.swift +18 -0
- package/ios/Source/audio/GlassesMicWatchdog.swift +33 -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 +201 -156
- 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 +269 -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 +24 -0
- 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/DemandDrivenAudioWorker.swift +150 -0
- package/ios/Source/utils/G2Text.swift +11 -0
- package/ios/Tests/BluetoothBackgroundModeTests.swift +32 -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/G2ConnectionSessionTests.swift +172 -0
- package/ios/Tests/G2TextTests.swift +10 -0
- package/ios/Tests/MentraLiveFileTransferSessionTests.swift +47 -0
- package/ios/Tests/MentraLiveReconnectPolicyTests.swift +58 -0
- package/ios/Tests/NimoCanvasCodecTests.swift +204 -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 +53 -0
- package/ios/Tests/VersionInfoResponseAccumulatorTests.swift +65 -0
- package/ios/Tests/VideoRecordingCommandTests.swift +81 -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 +14 -0
- package/src/_private/BluetoothSdkModule.ts +8 -0
- package/src/generated/changelogCatalog.ts +2 -2
- package/src/generated/releaseMetadata.ts +5 -5
- package/src/index.ts +1 -0
|
@@ -13,6 +13,7 @@ import androidx.core.content.ContextCompat
|
|
|
13
13
|
import com.mentra.bluetoothsdk.controllers.ControllerManager
|
|
14
14
|
import com.mentra.bluetoothsdk.controllers.R1
|
|
15
15
|
import com.mentra.bluetoothsdk.services.ForegroundService
|
|
16
|
+
import com.mentra.bluetoothsdk.services.G2ConnectionRecovery
|
|
16
17
|
import com.mentra.bluetoothsdk.services.PhoneMic
|
|
17
18
|
import com.mentra.bluetoothsdk.sgcs.Ar99
|
|
18
19
|
import com.mentra.bluetoothsdk.sgcs.G1
|
|
@@ -34,7 +35,10 @@ import com.mentra.bluetoothsdk.utils.MicTypes
|
|
|
34
35
|
import com.mentra.bluetoothsdk.utils.PhoneAudioMonitor
|
|
35
36
|
import com.mentra.lc3Lib.Lc3Cpp
|
|
36
37
|
import com.mentra.bluetoothsdk.stt.SherpaOnnxTranscriber
|
|
38
|
+
import kotlin.coroutines.resume
|
|
39
|
+
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
37
40
|
import kotlinx.coroutines.CoroutineScope
|
|
41
|
+
import kotlinx.coroutines.withContext
|
|
38
42
|
import kotlinx.coroutines.Dispatchers
|
|
39
43
|
import kotlinx.coroutines.launch
|
|
40
44
|
import java.text.SimpleDateFormat
|
|
@@ -45,12 +49,15 @@ import java.util.concurrent.Executors
|
|
|
45
49
|
import java.util.concurrent.TimeUnit
|
|
46
50
|
import kotlin.jvm.JvmStatic
|
|
47
51
|
|
|
48
|
-
class DeviceManager {
|
|
52
|
+
class DeviceManager internal constructor(initializeHardware: Boolean) {
|
|
53
|
+
constructor() : this(initializeHardware = true)
|
|
49
54
|
companion object {
|
|
50
55
|
|
|
51
56
|
@Volatile
|
|
52
57
|
private var _instance: DeviceManager? = null
|
|
53
58
|
|
|
59
|
+
internal fun isInitialized(): Boolean = _instance != null
|
|
60
|
+
|
|
54
61
|
@JvmStatic
|
|
55
62
|
fun getInstance(): DeviceManager {
|
|
56
63
|
return _instance
|
|
@@ -59,6 +66,10 @@ class DeviceManager {
|
|
|
59
66
|
}
|
|
60
67
|
|
|
61
68
|
// MARK: - Unique (Android)
|
|
69
|
+
private val connectionRecovery = G2ConnectionRecovery(Bridge.getContext())
|
|
70
|
+
private val recoveryLock = Any()
|
|
71
|
+
private var recoverG2Connection = false
|
|
72
|
+
private var recoveryListenerId: String? = null
|
|
62
73
|
private var serviceStarted = false
|
|
63
74
|
private val mainHandler = Handler(Looper.getMainLooper())
|
|
64
75
|
private val executor: ExecutorService = Executors.newSingleThreadExecutor()
|
|
@@ -79,7 +90,8 @@ class DeviceManager {
|
|
|
79
90
|
// MARK: - End Unique
|
|
80
91
|
|
|
81
92
|
// MARK: - Properties
|
|
82
|
-
|
|
93
|
+
// Read by the welcome executor as well as device lifecycle handlers.
|
|
94
|
+
@Volatile var sgc: SGCManager? = null
|
|
83
95
|
var controller: ControllerManager? = null
|
|
84
96
|
|
|
85
97
|
// settings:
|
|
@@ -283,7 +295,22 @@ class DeviceManager {
|
|
|
283
295
|
|
|
284
296
|
init {
|
|
285
297
|
Bridge.log("DeviceManager: init()")
|
|
298
|
+
recoveryListenerId = DeviceStore.store.addListener { category, changes ->
|
|
299
|
+
if (category == "bluetooth" && changes.keys.any { it in G2ConnectionRecovery.keys }) {
|
|
300
|
+
synchronized(recoveryLock) {
|
|
301
|
+
if (recoverG2Connection) {
|
|
302
|
+
connectionRecovery.save(DeviceStore.store.getCategory("bluetooth"))
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
286
307
|
initializeViewStates()
|
|
308
|
+
if (initializeHardware) initializeHardwareServices()
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Keep host routing usable without starting audio codecs, receivers or services.
|
|
312
|
+
// The public constructor always initializes the hardware environment.
|
|
313
|
+
private fun initializeHardwareServices() {
|
|
287
314
|
startForegroundService()
|
|
288
315
|
// setupPermissionMonitoring()
|
|
289
316
|
setupBluetoothStateMonitoring()
|
|
@@ -1354,7 +1381,8 @@ class DeviceManager {
|
|
|
1354
1381
|
Bridge.log("MAN: Cleaning up previous sgc type: ${sgc?.type}")
|
|
1355
1382
|
sgc?.cleanup()
|
|
1356
1383
|
sgc = null
|
|
1357
|
-
|
|
1384
|
+
DeviceStore.apply("glasses", "micEnabled", false)
|
|
1385
|
+
resetConnectionReadyState()
|
|
1358
1386
|
}
|
|
1359
1387
|
|
|
1360
1388
|
if (sgc != null) {
|
|
@@ -1477,12 +1505,16 @@ class DeviceManager {
|
|
|
1477
1505
|
return
|
|
1478
1506
|
}
|
|
1479
1507
|
|
|
1508
|
+
// A new identity must never inherit the previous glasses' address when
|
|
1509
|
+
// pairing supplied only a name. Same-device reconnects retain their cache.
|
|
1510
|
+
val identityChanged = pendingDeviceName.isNotEmpty() &&
|
|
1511
|
+
(pendingDeviceName != deviceName || sgc?.type != defaultWearable)
|
|
1512
|
+
if (identityChanged || pendingDeviceAddress.isNotEmpty()) {
|
|
1513
|
+
deviceAddress = pendingDeviceAddress
|
|
1514
|
+
}
|
|
1480
1515
|
if (pendingDeviceName.isNotEmpty()) {
|
|
1481
1516
|
deviceName = pendingDeviceName
|
|
1482
1517
|
}
|
|
1483
|
-
if (pendingDeviceAddress.isNotEmpty()) {
|
|
1484
|
-
deviceAddress = pendingDeviceAddress
|
|
1485
|
-
}
|
|
1486
1518
|
clearPendingConnection()
|
|
1487
1519
|
|
|
1488
1520
|
val readyKey = "${sgc?.type}:${deviceName}"
|
|
@@ -1497,6 +1529,10 @@ class DeviceManager {
|
|
|
1497
1529
|
Bridge.log("MAN: handleDeviceReady() ${sgc?.type}")
|
|
1498
1530
|
resetMicHealth()
|
|
1499
1531
|
defaultWearable = sgc?.type ?: ""
|
|
1532
|
+
synchronized(recoveryLock) {
|
|
1533
|
+
recoverG2Connection = defaultWearable == DeviceTypes.G2
|
|
1534
|
+
connectionRecovery.save(DeviceStore.store.getCategory("bluetooth"))
|
|
1535
|
+
}
|
|
1500
1536
|
searching = false
|
|
1501
1537
|
|
|
1502
1538
|
syncSystemTimeOnceForConnection(readyKey)
|
|
@@ -1504,6 +1540,7 @@ class DeviceManager {
|
|
|
1504
1540
|
// re-apply display height/depth after reconnection
|
|
1505
1541
|
mainHandler.postDelayed(
|
|
1506
1542
|
{
|
|
1543
|
+
val device = sgc ?: return@postDelayed
|
|
1507
1544
|
val h =
|
|
1508
1545
|
(DeviceStore.store.get("bluetooth", "dashboard_height") as? Number)
|
|
1509
1546
|
?.toInt()
|
|
@@ -1512,19 +1549,24 @@ class DeviceManager {
|
|
|
1512
1549
|
(DeviceStore.store.get("bluetooth", "dashboard_depth") as? Number)
|
|
1513
1550
|
?.toInt()
|
|
1514
1551
|
?: dashboardDepth // canonical default (2), not 1
|
|
1515
|
-
val d = rawDepth.coerceIn(1, 4)
|
|
1516
|
-
|
|
1552
|
+
val d = if (device.type == DeviceTypes.NIMO) rawDepth.coerceIn(0, 10) else rawDepth.coerceIn(1, 4)
|
|
1553
|
+
device.setDashboardPosition(h, d)
|
|
1517
1554
|
},
|
|
1518
1555
|
2000
|
|
1519
1556
|
)
|
|
1520
1557
|
|
|
1521
|
-
//
|
|
1558
|
+
// A full-canvas device can opt out so this temporary message does not
|
|
1559
|
+
// erase the host's connected-edge scene replay.
|
|
1522
1560
|
if (shouldSendBootingMessage) {
|
|
1523
1561
|
shouldSendBootingMessage = false
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1562
|
+
val device = sgc
|
|
1563
|
+
if (device?.showConnectionConfirmation == true) {
|
|
1564
|
+
executor.execute {
|
|
1565
|
+
if (sgc !== device) return@execute
|
|
1566
|
+
device.sendTextWall("// MentraOS Connected")
|
|
1567
|
+
Thread.sleep(3000)
|
|
1568
|
+
if (sgc === device) device.clearDisplay()
|
|
1569
|
+
}
|
|
1528
1570
|
}
|
|
1529
1571
|
}
|
|
1530
1572
|
|
|
@@ -1537,10 +1579,11 @@ class DeviceManager {
|
|
|
1537
1579
|
handleMach1Ready() // Z100 uses same initialization as Mach1
|
|
1538
1580
|
}
|
|
1539
1581
|
|
|
1540
|
-
//
|
|
1541
|
-
//
|
|
1582
|
+
// Disconnect clears micEnabled but preserves the consumers' audio requests.
|
|
1583
|
+
// Recompute that derived flag before selecting a microphone; replaying an
|
|
1584
|
+
// unchanged should_send_* setting is deduplicated by DeviceStore.apply().
|
|
1542
1585
|
Bridge.log("MAN: Re-applying microphone settings after reconnection")
|
|
1543
|
-
|
|
1586
|
+
setMicState()
|
|
1544
1587
|
|
|
1545
1588
|
// send to the server our battery status:
|
|
1546
1589
|
Bridge.sendBatteryStatus(sgc?.batteryLevel ?: -1, false)
|
|
@@ -1580,7 +1623,11 @@ class DeviceManager {
|
|
|
1580
1623
|
mainHandler.postDelayed(sync, 3000)
|
|
1581
1624
|
}
|
|
1582
1625
|
|
|
1583
|
-
private fun
|
|
1626
|
+
private fun resetConnectionReadyState() {
|
|
1627
|
+
// Suppress duplicate readiness only within one connection, never across
|
|
1628
|
+
// a genuine disconnect/reconnect of the same glasses inside two seconds.
|
|
1629
|
+
lastReadyHandledKey = ""
|
|
1630
|
+
lastReadyHandledAtMs = 0L
|
|
1584
1631
|
pendingSystemTimeSync?.let { mainHandler.removeCallbacks(it) }
|
|
1585
1632
|
pendingSystemTimeSync = null
|
|
1586
1633
|
lastSystemTimeSyncConnectionKey = ""
|
|
@@ -1598,7 +1645,7 @@ class DeviceManager {
|
|
|
1598
1645
|
|
|
1599
1646
|
fun handleDeviceDisconnected() {
|
|
1600
1647
|
Bridge.log("MAN: Device disconnected")
|
|
1601
|
-
|
|
1648
|
+
resetConnectionReadyState()
|
|
1602
1649
|
resetMicHealth()
|
|
1603
1650
|
DeviceStore.apply("glasses", "headUp", false)
|
|
1604
1651
|
DeviceStore.apply(
|
|
@@ -1677,7 +1724,7 @@ class DeviceManager {
|
|
|
1677
1724
|
dashboardSceneCleanupPending = false
|
|
1678
1725
|
val elementIds = pendingDashboardSceneElementIds.toList()
|
|
1679
1726
|
pendingDashboardSceneElementIds.clear()
|
|
1680
|
-
if (elementIds.isNotEmpty()) {
|
|
1727
|
+
if (elementIds.isNotEmpty() && sgc?.sceneHandoffRequiresClear == true) {
|
|
1681
1728
|
sgc?.clearSceneElements(elementIds)
|
|
1682
1729
|
}
|
|
1683
1730
|
}
|
|
@@ -1714,7 +1761,7 @@ class DeviceManager {
|
|
|
1714
1761
|
sceneStates[stateIndex] = null
|
|
1715
1762
|
if (stateIndex == 1 && dashboardSceneCleanupPending) {
|
|
1716
1763
|
pendingDashboardSceneElementIds.addAll(prevFrame.elements.map { it.id })
|
|
1717
|
-
} else if (layoutType != "clear_view") {
|
|
1764
|
+
} else if (layoutType != "clear_view" && shouldClearSceneHandoff(stateIndex)) {
|
|
1718
1765
|
sgc?.clearSceneElements(prevFrame.elements.map { it.id })
|
|
1719
1766
|
}
|
|
1720
1767
|
}
|
|
@@ -1781,7 +1828,7 @@ class DeviceManager {
|
|
|
1781
1828
|
val prevLegacyType = viewStates[stateIndex].layoutType
|
|
1782
1829
|
val cleanupDeferred = stateIndex == 1 && dashboardSceneCleanupPending
|
|
1783
1830
|
if (
|
|
1784
|
-
!cleanupDeferred &&
|
|
1831
|
+
!cleanupDeferred && shouldClearSceneHandoff(stateIndex) &&
|
|
1785
1832
|
prevLegacyType.isNotEmpty() &&
|
|
1786
1833
|
prevLegacyType != "clear_view" &&
|
|
1787
1834
|
prevLegacyType != "scene"
|
|
@@ -1797,7 +1844,7 @@ class DeviceManager {
|
|
|
1797
1844
|
// blank.
|
|
1798
1845
|
if (stateIndex == 1 && dashboardSceneCleanupPending) {
|
|
1799
1846
|
pendingDashboardSceneElementIds.addAll(prevFrame.elements.map { it.id })
|
|
1800
|
-
} else {
|
|
1847
|
+
} else if (shouldClearSceneHandoff(stateIndex)) {
|
|
1801
1848
|
sgc?.clearSceneElements(prevFrame.elements.map { it.id })
|
|
1802
1849
|
}
|
|
1803
1850
|
frame = frame.copy(replay = true, elements = frame.elements.map { it.copy(change = "created") })
|
|
@@ -1816,6 +1863,15 @@ class DeviceManager {
|
|
|
1816
1863
|
}
|
|
1817
1864
|
}
|
|
1818
1865
|
|
|
1866
|
+
// Hidden-view updates only change their replay slot. They must never clear
|
|
1867
|
+
// the visible view, and full-frame adapters need no separate handoff sweep.
|
|
1868
|
+
private fun shouldClearSceneHandoff(stateIndex: Int): Boolean {
|
|
1869
|
+
val visibleIndex = if (headUp && contextualDashboard) 1 else 0
|
|
1870
|
+
val device = sgc ?: return false
|
|
1871
|
+
return stateIndex == visibleIndex && !screenDisabled && device.fullyBooted &&
|
|
1872
|
+
!device.type.contains(DeviceTypes.SIMULATED) && device.sceneHandoffRequiresClear
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1819
1875
|
/** Guarded scene dispatch — mirrors sendCurrentState's send conditions. */
|
|
1820
1876
|
private fun dispatchSceneFrame(stateIndex: Int, frame: SceneFrame) {
|
|
1821
1877
|
if (screenDisabled) return
|
|
@@ -2389,10 +2445,20 @@ class DeviceManager {
|
|
|
2389
2445
|
}
|
|
2390
2446
|
|
|
2391
2447
|
fun disconnect(clearPendingIdentity: Boolean = true) {
|
|
2448
|
+
synchronized(recoveryLock) {
|
|
2449
|
+
recoverG2Connection = false
|
|
2450
|
+
connectionRecovery.clear()
|
|
2451
|
+
}
|
|
2392
2452
|
sgc?.clearDisplay()
|
|
2393
|
-
|
|
2453
|
+
// NIMO owns a background canvas encoder and this path discards its instance.
|
|
2454
|
+
// A link-level reconnect keeps the instance (and encoder) through disconnect().
|
|
2455
|
+
val device = sgc
|
|
2456
|
+
if (device is Nimo) device.cleanup() else device?.disconnect()
|
|
2394
2457
|
sgc = null // Clear the SGC reference after disconnect
|
|
2395
|
-
|
|
2458
|
+
// This cache belongs to the discarded connection. Keep consumer demand,
|
|
2459
|
+
// but require a new mic-enable command when replacement glasses are ready.
|
|
2460
|
+
DeviceStore.apply("glasses", "micEnabled", false)
|
|
2461
|
+
resetConnectionReadyState()
|
|
2396
2462
|
resetMicHealth()
|
|
2397
2463
|
searching = false
|
|
2398
2464
|
micEnabled = false
|
|
@@ -2439,7 +2505,42 @@ class DeviceManager {
|
|
|
2439
2505
|
controller = null
|
|
2440
2506
|
}
|
|
2441
2507
|
|
|
2508
|
+
private var unpairInProgress = false
|
|
2509
|
+
|
|
2510
|
+
/** Explicit user Unpair, separate from passive pairing cleanup and logout. */
|
|
2511
|
+
suspend fun unpair() = withContext(Dispatchers.Main) {
|
|
2512
|
+
check(!unpairInProgress) { "Unpair already in progress" }
|
|
2513
|
+
unpairInProgress = true
|
|
2514
|
+
try {
|
|
2515
|
+
val target = sgc
|
|
2516
|
+
val nimo = target as? Nimo
|
|
2517
|
+
val savedNimoAddress = when {
|
|
2518
|
+
defaultWearable == DeviceTypes.NIMO -> deviceAddress
|
|
2519
|
+
pendingWearable == DeviceTypes.NIMO -> pendingDeviceAddress
|
|
2520
|
+
else -> null
|
|
2521
|
+
}
|
|
2522
|
+
if (nimo != null) {
|
|
2523
|
+
suspendCancellableCoroutine<Boolean> { continuation ->
|
|
2524
|
+
nimo.resetForUnpair { if (continuation.isActive) continuation.resume(it) }
|
|
2525
|
+
}
|
|
2526
|
+
check(sgc === target) { "Glasses changed during Unpair" }
|
|
2527
|
+
// Drop the encrypted link before removing the phone's bond.
|
|
2528
|
+
nimo.disconnect()
|
|
2529
|
+
nimo.removeBluetoothBond()
|
|
2530
|
+
} else if (target == null && savedNimoAddress != null) {
|
|
2531
|
+
Nimo.removeBluetoothBond(savedNimoAddress)
|
|
2532
|
+
}
|
|
2533
|
+
forget()
|
|
2534
|
+
} finally {
|
|
2535
|
+
unpairInProgress = false
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2442
2539
|
fun forget() {
|
|
2540
|
+
synchronized(recoveryLock) {
|
|
2541
|
+
recoverG2Connection = false
|
|
2542
|
+
connectionRecovery.clear()
|
|
2543
|
+
}
|
|
2443
2544
|
Bridge.log("MAN: Forgetting smart glasses")
|
|
2444
2545
|
|
|
2445
2546
|
val live = sgc as? MentraLive
|
|
@@ -2451,7 +2552,8 @@ class DeviceManager {
|
|
|
2451
2552
|
// session state without calling disconnect() again (that would hit a dead instance
|
|
2452
2553
|
// and leave a destroyed MentraLive retained for the next scan).
|
|
2453
2554
|
sgc = null
|
|
2454
|
-
|
|
2555
|
+
DeviceStore.apply("glasses", "micEnabled", false)
|
|
2556
|
+
resetConnectionReadyState()
|
|
2455
2557
|
searching = false
|
|
2456
2558
|
micEnabled = false
|
|
2457
2559
|
updateMicState()
|
|
@@ -2554,6 +2656,8 @@ class DeviceManager {
|
|
|
2554
2656
|
|
|
2555
2657
|
// MARK: Cleanup
|
|
2556
2658
|
fun cleanup() {
|
|
2659
|
+
recoveryListenerId?.let { DeviceStore.store.removeListener(it) }
|
|
2660
|
+
recoveryListenerId = null
|
|
2557
2661
|
stopBluetoothStateMonitoring()
|
|
2558
2662
|
|
|
2559
2663
|
micReinitRunnable?.let { mainHandler.removeCallbacks(it) }
|
|
@@ -142,6 +142,7 @@ object DeviceStore {
|
|
|
142
142
|
store.set("bluetooth", "gallery_mode", true)
|
|
143
143
|
store.set("bluetooth", "voice_activity_detection_enabled", BluetoothSdkDefaults.VOICE_ACTIVITY_DETECTION_ENABLED)
|
|
144
144
|
store.set("bluetooth", "loudness_gate_enabled", BluetoothSdkDefaults.LOUDNESS_GATE_ENABLED)
|
|
145
|
+
store.set("bluetooth", "auto_power_off_enabled", BluetoothSdkDefaults.AUTO_POWER_OFF_ENABLED)
|
|
145
146
|
store.set("bluetooth", "screen_disabled", false)
|
|
146
147
|
store.set("bluetooth", "button_photo_size", "max")
|
|
147
148
|
store.set("bluetooth", "button_max_recording_time", 10)
|
|
@@ -219,14 +220,30 @@ object DeviceStore {
|
|
|
219
220
|
}
|
|
220
221
|
|
|
221
222
|
// BLUETOOTH:
|
|
223
|
+
"bluetooth" to "contextual_dashboard" -> {
|
|
224
|
+
if (value is Boolean) {
|
|
225
|
+
CoroutineScope(Dispatchers.Main.immediate).launch {
|
|
226
|
+
// The setting changes the selected view even without a new head event.
|
|
227
|
+
// Re-read state on Main and retain the manager's readiness/display guards.
|
|
228
|
+
if (store.get("glasses", "headUp") == true) {
|
|
229
|
+
DeviceManager.getInstance().sendCurrentState()
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
222
234
|
"bluetooth" to "brightness" -> {
|
|
223
235
|
val b = (value as? Number)?.toInt() ?: 50
|
|
224
236
|
val auto = (store.get("bluetooth", "auto_brightness") as? Boolean) ?: true
|
|
225
237
|
CoroutineScope(Dispatchers.Main).launch {
|
|
226
|
-
DeviceManager.getInstance().sgc
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
238
|
+
val device = DeviceManager.getInstance().sgc ?: return@launch
|
|
239
|
+
device.setBrightness(b, auto)
|
|
240
|
+
if (device.showBrightnessConfirmation) {
|
|
241
|
+
device.sendTextWall("Set brightness to $b%")
|
|
242
|
+
delay(800) // 0.8 seconds
|
|
243
|
+
if (DeviceManager.getInstance().sgc === device) {
|
|
244
|
+
device.clearDisplay()
|
|
245
|
+
}
|
|
246
|
+
}
|
|
230
247
|
}
|
|
231
248
|
}
|
|
232
249
|
"bluetooth" to "auto_brightness" -> {
|
|
@@ -234,16 +251,17 @@ object DeviceStore {
|
|
|
234
251
|
val auto = (value as? Boolean) ?: true
|
|
235
252
|
val autoBrightnessChanged = (oldValue as? Boolean) != auto
|
|
236
253
|
CoroutineScope(Dispatchers.Main).launch {
|
|
237
|
-
DeviceManager.getInstance().sgc
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
)
|
|
254
|
+
val device = DeviceManager.getInstance().sgc ?: return@launch
|
|
255
|
+
device.setBrightness(b, auto)
|
|
256
|
+
if (autoBrightnessChanged && device.showBrightnessConfirmation) {
|
|
257
|
+
device.sendTextWall(
|
|
258
|
+
if (auto) "Enabled auto brightness"
|
|
259
|
+
else "Disabled auto brightness"
|
|
260
|
+
)
|
|
245
261
|
delay(800) // 0.8 seconds
|
|
246
|
-
DeviceManager.getInstance().sgc
|
|
262
|
+
if (DeviceManager.getInstance().sgc === device) {
|
|
263
|
+
device.clearDisplay()
|
|
264
|
+
}
|
|
247
265
|
}
|
|
248
266
|
}
|
|
249
267
|
}
|
|
@@ -291,6 +309,12 @@ object DeviceStore {
|
|
|
291
309
|
"bluetooth" to "loudness_gate_enabled" -> {
|
|
292
310
|
DeviceManager.getInstance().sgc?.sendLoudnessGateSetting()
|
|
293
311
|
}
|
|
312
|
+
"bluetooth" to "auto_power_off_enabled" -> {
|
|
313
|
+
Bridge.log(
|
|
314
|
+
"DeviceStore: auto_power_off_enabled changed to $value — sending cs_swit type 11"
|
|
315
|
+
)
|
|
316
|
+
DeviceManager.getInstance().sgc?.sendAutoPowerOffSetting()
|
|
317
|
+
}
|
|
294
318
|
// Deliberately has no seeded default above: the key starts absent so
|
|
295
319
|
// that a session where the engine has not authorized tuning can only
|
|
296
320
|
// ever send a reset.
|
|
@@ -3,8 +3,8 @@ package com.mentra.bluetoothsdk
|
|
|
3
3
|
/** Generated from /changelogs. Do not edit directly. */
|
|
4
4
|
internal val GENERATED_RELEASE_CHANGELOGS: List<ReleaseChangelog> =
|
|
5
5
|
listOf(
|
|
6
|
-
ReleaseChangelog(version = "3.2.1", markdown = "
|
|
7
|
-
ReleaseChangelog(version = "3.2.0", markdown = "
|
|
6
|
+
ReleaseChangelog(version = "3.2.1", markdown = "- **Livestreamer:** \"Stream Here\" now works without a Wi-Fi network. Improved stream startup and connection reliability.\n- **Photos and gallery:** More reliable photo capture and gallery syncing.\n- **Mentra AI:** Reliability improvements and bug fixes.\n- **Miniapps:** Smoother transitions, with back buttons and gestures that take you to the previous screen inside a miniapp.\n- **Pairing and connections:** Improved G1 pairing and G2 background reconnection.\n- **Glasses updates:** Fixed repeated update checks and improved recovery after interrupted updates.\n- **Mentra Maps:** Bug fixes.\n- **NIMO:** Improved text and map layouts.\n- **Self-hosting:** Connect the Mentra App to your own self-hosted Mentra backend."),
|
|
7
|
+
ReleaseChangelog(version = "3.2.0", markdown = "- Improved glasses connectivity and Wi-Fi setup."),
|
|
8
8
|
ReleaseChangelog(version = "3.1.1", markdown = "- Improved Google login on iOS.\n- Pairing no longer requires phone microphone access."),
|
|
9
9
|
ReleaseChangelog(version = "3.1.0", markdown = "**Mentra App and glasses**\n\n- Improved glasses pairing and reconnection, including on iPad.\n- Improved Android background reliability for captions and miniapps.\n- Added Xingyi AR99 support.\n- More reliable photo and video capture, with improved shutter sounds and privacy-light timing. Taking photos no longer interrupts your running miniapp.\n- Added video thumbnails and smoother gallery browsing.\n- Easier Mentra Live updates through your phone, with clearer progress and better recovery from interruptions.\n\n**Miniapp updates**\n\n- **Mentra AI:** Control whether photos are included with questions. Improved visual answers and connected-app follow-ups.\n- **Captions:** Added offline transcription settings and top/bottom positioning on supported displays.\n- **Translation:** Restored all 60 languages.\n- **Mentra Maps:** Clearer directions on G1/G2, less display flickering, and improved Android offline routing.\n- **Livestreamer:** More video-quality controls and custom streaming options, plus improved startup and sharing.\n- **Merge:** Tap to expand an insight; swipe down or back to dismiss it.\n- **Mentra Notes:** Smoother back navigation.\n- **Recorder:** Fixed audio sharing."),
|
|
10
10
|
)
|
|
@@ -3,9 +3,9 @@ package com.mentra.bluetoothsdk
|
|
|
3
3
|
/** Generated by release CI. Do not edit in a release checkout. */
|
|
4
4
|
internal object GeneratedReleaseMetadata {
|
|
5
5
|
const val FAMILY_BASE_VERSION: String = "3.2.1"
|
|
6
|
-
const val RELEASE_IDENTITY: String = "3.2.1
|
|
7
|
-
const val RELEASE_SET_ID: String = "mentra-3.2.1
|
|
8
|
-
const val SOURCE_COMMIT: String = "
|
|
9
|
-
const val OTA_MANIFEST_URL: String = "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.2.1/mentra-live-ota-3.2.1-
|
|
10
|
-
const val OTA_MANIFEST_SHA256: String = "
|
|
6
|
+
const val RELEASE_IDENTITY: String = "3.2.1"
|
|
7
|
+
const val RELEASE_SET_ID: String = "mentra-3.2.1"
|
|
8
|
+
const val SOURCE_COMMIT: String = "be56e1456c69f681985642f376ed3d397ec022cc"
|
|
9
|
+
const val OTA_MANIFEST_URL: String = "https://artifactscdn.mentraglass.com/Mentra-Community/MentraOS/releases/mentra-builds-v3.2.1/mentra-live-ota-3.2.1-beta.578.json"
|
|
10
|
+
const val OTA_MANIFEST_SHA256: String = "21af62269e29fd2ee6b9737745a895694bc221672b191d80795057b5a832f400"
|
|
11
11
|
}
|
|
@@ -651,6 +651,10 @@ class MentraBluetoothSdk private constructor(
|
|
|
651
651
|
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "loudness_gate_enabled", enabled)
|
|
652
652
|
}
|
|
653
653
|
|
|
654
|
+
fun setAutoPowerOffEnabled(enabled: Boolean) {
|
|
655
|
+
DeviceStore.apply(ObservableStore.BLUETOOTH_CATEGORY, "auto_power_off_enabled", enabled)
|
|
656
|
+
}
|
|
657
|
+
|
|
654
658
|
@Deprecated(
|
|
655
659
|
message =
|
|
656
660
|
"Sticky action-button photo presets are deprecated. Prefer per-request " +
|
|
@@ -6,13 +6,14 @@ internal sealed interface VersionInfoAccumulatorOutcome {
|
|
|
6
6
|
data class Complete(val result: VersionInfoResult) : VersionInfoAccumulatorOutcome
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
/** One request, explicit modern completion, or the
|
|
9
|
+
/** One request, explicit modern completion, or the legacy firmware's terminal chunk. */
|
|
10
10
|
internal class VersionInfoResponseAccumulator(private val expectedRequestId: String) {
|
|
11
11
|
private val values = mutableMapOf<String, Any>()
|
|
12
12
|
private val chunks = mutableMapOf<Int, Map<String, Any>>()
|
|
13
13
|
private var count: Int? = null
|
|
14
14
|
private var sid: String? = null
|
|
15
15
|
private var legacyStarted = false
|
|
16
|
+
private var legacyTwoChunkResponse = false
|
|
16
17
|
private var completed = false
|
|
17
18
|
|
|
18
19
|
fun accept(event: Map<String, Any>): VersionInfoAccumulatorOutcome {
|
|
@@ -41,10 +42,20 @@ internal class VersionInfoResponseAccumulator(private val expectedRequestId: Str
|
|
|
41
42
|
if (count != null) return VersionInfoAccumulatorOutcome.Ignored
|
|
42
43
|
when (event[RESPONSE_CHUNK_KEY] as? String ?: "version_info") {
|
|
43
44
|
"version_info" -> { values.clear(); merge(event); return finish() }
|
|
44
|
-
"version_info_1" -> {
|
|
45
|
+
"version_info_1" -> {
|
|
46
|
+
values.clear()
|
|
47
|
+
legacyStarted = true
|
|
48
|
+
legacyTwoChunkResponse = event["buildNumber"] == "27"
|
|
49
|
+
merge(event)
|
|
50
|
+
}
|
|
45
51
|
"version_info_2" -> {
|
|
46
52
|
if (!legacyStarted) return VersionInfoAccumulatorOutcome.Ignored
|
|
53
|
+
// Factory ASG27 sends only chunks 1 and 2. Its second chunk carries the
|
|
54
|
+
// OTA URL and legacy firmware version; ASG31+ moves firmware to chunk 3.
|
|
47
55
|
merge(event)
|
|
56
|
+
if (legacyTwoChunkResponse && values["buildNumber"] == "27" &&
|
|
57
|
+
!(event["otaVersionUrl"] as? String).isNullOrBlank()
|
|
58
|
+
) return finish()
|
|
48
59
|
}
|
|
49
60
|
"version_info_3" -> {
|
|
50
61
|
if (!legacyStarted) return VersionInfoAccumulatorOutcome.Ignored
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
package com.mentra.bluetoothsdk
|
|
2
2
|
|
|
3
3
|
internal object DashboardContentFormatter {
|
|
4
|
-
const val STATUS_HEADER = "\$TIME12$ \$DATE$ \$GBATT$
|
|
4
|
+
const val STATUS_HEADER = "\$TIME12$ \$DATE$ \$GBATT$"
|
|
5
5
|
|
|
6
6
|
fun template(content: String): String =
|
|
7
7
|
if (content.isEmpty()) STATUS_HEADER else "$STATUS_HEADER\n\n$content"
|
|
@@ -13,6 +13,8 @@ import android.os.IBinder
|
|
|
13
13
|
import androidx.core.app.NotificationCompat
|
|
14
14
|
import androidx.core.content.ContextCompat
|
|
15
15
|
import com.mentra.bluetoothsdk.Bridge
|
|
16
|
+
import com.mentra.bluetoothsdk.DeviceManager
|
|
17
|
+
import com.mentra.bluetoothsdk.DeviceStore
|
|
16
18
|
import com.mentra.bluetoothsdk.debug.BleTraceLogger
|
|
17
19
|
|
|
18
20
|
class ForegroundService : Service() {
|
|
@@ -107,9 +109,40 @@ class ForegroundService : Service() {
|
|
|
107
109
|
}
|
|
108
110
|
// Re-check permissions in case they changed
|
|
109
111
|
startForegroundWithAutoDetectedType()
|
|
112
|
+
// A null intent is Android restarting START_STICKY after process death.
|
|
113
|
+
// Do not wait for an Activity/Expo module to restore the native BLE owner.
|
|
114
|
+
if (intent == null && !DeviceManager.isInitialized()) {
|
|
115
|
+
val settings = G2ConnectionRecovery(this).read()
|
|
116
|
+
if (settings != null && !startHostRuntimeRecovery()) {
|
|
117
|
+
Bridge.initialize(applicationContext)
|
|
118
|
+
settings.forEach { (key, value) -> DeviceStore.set("bluetooth", key, value) }
|
|
119
|
+
Bridge.log("ForegroundService: restoring G2 connection after process restart")
|
|
120
|
+
try {
|
|
121
|
+
DeviceManager.getInstance().connectDefault()
|
|
122
|
+
} catch (error: Exception) {
|
|
123
|
+
Bridge.log("ForegroundService: G2 recovery failed: ${error.message}")
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
110
127
|
return START_STICKY
|
|
111
128
|
}
|
|
112
129
|
|
|
130
|
+
private fun startHostRuntimeRecovery(): Boolean {
|
|
131
|
+
// Keep the standalone SDK free of React dependencies. The Mentra App
|
|
132
|
+
// opts in with a private headless service declared by its host module.
|
|
133
|
+
val serviceName = packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA)
|
|
134
|
+
.metaData?.getString("com.mentra.bluetoothsdk.RUNTIME_RECOVERY_SERVICE")
|
|
135
|
+
?: return false
|
|
136
|
+
return try {
|
|
137
|
+
if (startService(Intent().setClassName(packageName, serviceName)) == null) return false
|
|
138
|
+
Bridge.log("ForegroundService: requested host JavaScript runtime recovery")
|
|
139
|
+
true
|
|
140
|
+
} catch (error: Exception) {
|
|
141
|
+
Bridge.log("ForegroundService: host runtime recovery could not start: ${error.message}")
|
|
142
|
+
false
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
113
146
|
private fun startForegroundWithAutoDetectedType() {
|
|
114
147
|
startForegroundWithType(detectServiceType())
|
|
115
148
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.services
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import com.mentra.bluetoothsdk.utils.DeviceTypes
|
|
5
|
+
import org.json.JSONObject
|
|
6
|
+
|
|
7
|
+
/** Native recovery of the last active G2 link when Android restarts our sticky service. */
|
|
8
|
+
internal class G2ConnectionRecovery(context: Context) {
|
|
9
|
+
private val preferences = context.getSharedPreferences("g2_connection_recovery", Context.MODE_PRIVATE)
|
|
10
|
+
|
|
11
|
+
companion object {
|
|
12
|
+
// Deliberately exclude transient connection flags, mic intent, tokens and pending pairing.
|
|
13
|
+
internal val keys = setOf(
|
|
14
|
+
"default_wearable", "device_name", "device_address", "project_name",
|
|
15
|
+
"brightness", "auto_brightness", "dashboard_height", "dashboard_depth",
|
|
16
|
+
"head_up_angle", "use_native_dashboard", "twelve_hour_time", "metric_system",
|
|
17
|
+
"screen_disabled", "power_saving_mode", "lc3_frame_size",
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
fun save(settings: Map<String, Any>) {
|
|
22
|
+
if (settings["default_wearable"] != DeviceTypes.G2 ||
|
|
23
|
+
(settings["device_name"] as? String).isNullOrBlank()) {
|
|
24
|
+
clear()
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
val snapshot = settings.filterKeys { it in keys }
|
|
28
|
+
preferences.edit().putString("snapshot", JSONObject(snapshot).toString()).apply()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
fun clear() {
|
|
32
|
+
preferences.edit().remove("snapshot").apply()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
fun read(): Map<String, Any>? {
|
|
36
|
+
val raw = preferences.getString("snapshot", null) ?: return null
|
|
37
|
+
return try {
|
|
38
|
+
val json = JSONObject(raw)
|
|
39
|
+
if (json.optString("default_wearable") != DeviceTypes.G2 ||
|
|
40
|
+
json.optString("device_name").isBlank()) return null
|
|
41
|
+
keys.filter { json.has(it) && !json.isNull(it) }.associateWith { json.get(it) }
|
|
42
|
+
} catch (_: Exception) {
|
|
43
|
+
clear()
|
|
44
|
+
null
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -1370,8 +1370,8 @@ class G1 : SGCManager() {
|
|
|
1370
1370
|
context!!.registerReceiver(bondingReceiver, filter)
|
|
1371
1371
|
isBondingReceiverRegistered = true
|
|
1372
1372
|
|
|
1373
|
-
|
|
1374
|
-
|
|
1373
|
+
// Keep the target supplied by connectById. device_name still belongs to
|
|
1374
|
+
// the previous pairing until DeviceManager promotes the ready device.
|
|
1375
1375
|
if (!bluetoothAdapter!!.isEnabled()) {
|
|
1376
1376
|
return
|
|
1377
1377
|
}
|