@mentra/bluetooth-sdk 3.2.0-dev.200 → 3.2.0-dev.206
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 +46 -45
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +5 -9
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +63 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +5 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedChangelogCatalog.kt +1 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +298 -181
- package/android/src/main/java/com/mentra/bluetoothsdk/MessageAckPolicy.kt +4 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/WifiRequestResolution.kt +246 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioConnectionTracker.kt +104 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +326 -45
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +2 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +14 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamControllerProbe.kt +14 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +16 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamSessionState.kt +32 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/GlassesStatusClassicAudioTest.kt +14 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/MessageAckPolicyTest.kt +16 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/WifiRequestResolutionTest.kt +271 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/ClassicAudioConnectionTrackerTest.kt +131 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/streaming/StreamControllerProbeTest.kt +24 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/streaming/StreamSessionStateTest.kt +43 -0
- package/build/BluetoothSdk.types.d.ts +68 -2
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +3 -4
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/generated/changelogCatalog.d.ts +1 -1
- package/build/generated/changelogCatalog.js +1 -1
- 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 +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +3 -0
- package/build/index.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +8 -13
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/Bridge.swift +63 -0
- package/ios/Source/DeviceManager.swift +6 -3
- package/ios/Source/GeneratedChangelogCatalog.swift +1 -1
- package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
- package/ios/Source/MentraBluetoothSDK.swift +372 -220
- package/ios/Source/WifiRequestResolution.swift +251 -0
- package/ios/Source/sgcs/MentraLive.swift +216 -70
- package/ios/Source/sgcs/SGCManager.swift +13 -1
- package/ios/Source/status/DeviceStatus.swift +28 -0
- package/ios/Source/streaming/StreamControllerProbe.swift +18 -0
- package/ios/Source/streaming/StreamModels.swift +12 -0
- package/ios/Source/streaming/StreamSessionState.swift +29 -0
- package/ios/Tests/StreamControllerProbeTests.swift +27 -0
- package/ios/Tests/StreamSessionStateTests.swift +38 -0
- package/ios/Tests/WifiRequestResolutionTests.swift +320 -0
- package/package.json +1 -1
- package/src/BluetoothSdk.types.ts +82 -2
- package/src/_private/BluetoothSdkModule.ts +4 -4
- package/src/generated/changelogCatalog.ts +1 -1
- package/src/generated/releaseMetadata.ts +5 -5
- package/src/index.ts +11 -0
|
@@ -39,6 +39,8 @@ import com.mentra.bluetoothsdk.PhotoSize
|
|
|
39
39
|
import com.mentra.bluetoothsdk.PhotoMode
|
|
40
40
|
import com.mentra.bluetoothsdk.DeviceStore
|
|
41
41
|
import com.mentra.bluetoothsdk.ObservableStore
|
|
42
|
+
import com.mentra.bluetoothsdk.incomingGlassesMessageAckId
|
|
43
|
+
import com.mentra.bluetoothsdk.wifiResponseEnvelopeIsValid
|
|
42
44
|
import com.mentra.bluetoothsdk.debug.BleTraceLogger
|
|
43
45
|
import com.mentra.bluetoothsdk.utils.BlePhotoUploadService
|
|
44
46
|
import com.mentra.bluetoothsdk.utils.ConnTypes
|
|
@@ -600,9 +602,14 @@ class MentraLive : SGCManager() {
|
|
|
600
602
|
|
|
601
603
|
// CTKD (Cross-Transport Key Derivation) support for BES devices
|
|
602
604
|
private var isBondingReceiverRegistered = false
|
|
603
|
-
private var isBtClassicConnected = false
|
|
604
605
|
private var bondingReceiver: BroadcastReceiver? = null
|
|
605
606
|
private var bondingRetryCount = 0
|
|
607
|
+
private var classicAudioReceiver: BroadcastReceiver? = null
|
|
608
|
+
private var isClassicAudioReceiverRegistered = false
|
|
609
|
+
private val classicAudioConnectionTracker =
|
|
610
|
+
ClassicAudioConnectionTracker { connected ->
|
|
611
|
+
DeviceStore.apply("glasses", "bluetoothClassicConnected", connected)
|
|
612
|
+
}
|
|
606
613
|
|
|
607
614
|
// Pairing timing diagnostics (filter logcat: PAIRING_TIMING)
|
|
608
615
|
private var pairingTimingActive = false
|
|
@@ -1004,6 +1011,8 @@ class MentraLive : SGCManager() {
|
|
|
1004
1011
|
|
|
1005
1012
|
// Initialize CTKD bonding receiver
|
|
1006
1013
|
initializeBondingReceiver()
|
|
1014
|
+
initializeClassicAudioReceiver()
|
|
1015
|
+
registerClassicAudioReceiver()
|
|
1007
1016
|
|
|
1008
1017
|
// Initialize the send queue processor
|
|
1009
1018
|
processSendQueueRunnable = Runnable {
|
|
@@ -2272,6 +2281,8 @@ class MentraLive : SGCManager() {
|
|
|
2272
2281
|
isConnecting = false
|
|
2273
2282
|
isConnected = true
|
|
2274
2283
|
connectedDevice = gatt.device
|
|
2284
|
+
classicAudioConnectionTracker.setTarget(gatt.device.address)
|
|
2285
|
+
refreshClassicAudioConnectionState(gatt.device)
|
|
2275
2286
|
emitConnectedPendingDeviceForPairingScan()
|
|
2276
2287
|
|
|
2277
2288
|
DeviceStore.apply("glasses", "bluetoothName", connectedDevice!!.name)
|
|
@@ -3305,8 +3316,12 @@ class MentraLive : SGCManager() {
|
|
|
3305
3316
|
return messageId
|
|
3306
3317
|
}
|
|
3307
3318
|
|
|
3308
|
-
/** Send a JSON object to the glasses with message ID and ACK tracking */
|
|
3319
|
+
/** Send a JSON object to the glasses with message ID and ACK tracking. */
|
|
3309
3320
|
private fun sendJson(json: JSONObject?, wakeup: Boolean) {
|
|
3321
|
+
sendJson(json, wakeup, true)
|
|
3322
|
+
}
|
|
3323
|
+
|
|
3324
|
+
private fun sendJson(json: JSONObject?, wakeup: Boolean, bridgeLogging: Boolean): Boolean {
|
|
3310
3325
|
if (json != null) {
|
|
3311
3326
|
try {
|
|
3312
3327
|
val sessionGeneration = bleSessionGeneration.get()
|
|
@@ -3325,7 +3340,7 @@ class MentraLive : SGCManager() {
|
|
|
3325
3340
|
json,
|
|
3326
3341
|
jsonStr.length
|
|
3327
3342
|
)
|
|
3328
|
-
sendDataToGlasses(jsonStr, wakeup, sessionGeneration)
|
|
3343
|
+
return sendDataToGlasses(jsonStr, wakeup, sessionGeneration, bridgeLogging)
|
|
3329
3344
|
} else {
|
|
3330
3345
|
// Add esoteric message ID to the JSON
|
|
3331
3346
|
val messageId = generateEsotericMessageId()
|
|
@@ -3350,12 +3365,13 @@ class MentraLive : SGCManager() {
|
|
|
3350
3365
|
// Calculate dynamic timeout for chunked message
|
|
3351
3366
|
val estimatedChunks = Math.ceil(jsonStr.length / 300.0).toInt()
|
|
3352
3367
|
ackTimeout = ACK_TIMEOUT_MS + (estimatedChunks * 50L) + 2000L
|
|
3353
|
-
|
|
3368
|
+
transportLog(
|
|
3354
3369
|
"LIVE: Message will be chunked into ~" +
|
|
3355
3370
|
estimatedChunks +
|
|
3356
3371
|
" chunks, using dynamic timeout: " +
|
|
3357
3372
|
ackTimeout +
|
|
3358
|
-
"ms"
|
|
3373
|
+
"ms",
|
|
3374
|
+
bridgeLogging,
|
|
3359
3375
|
)
|
|
3360
3376
|
}
|
|
3361
3377
|
} catch (e: JSONException) {
|
|
@@ -3367,7 +3383,13 @@ class MentraLive : SGCManager() {
|
|
|
3367
3383
|
}
|
|
3368
3384
|
|
|
3369
3385
|
// Track the message for ACK with appropriate timeout
|
|
3370
|
-
trackMessageForAck(
|
|
3386
|
+
trackMessageForAck(
|
|
3387
|
+
messageId,
|
|
3388
|
+
jsonStr,
|
|
3389
|
+
ackTimeout,
|
|
3390
|
+
sessionGeneration,
|
|
3391
|
+
bridgeLogging,
|
|
3392
|
+
)
|
|
3371
3393
|
|
|
3372
3394
|
// Send the data
|
|
3373
3395
|
if ("take_photo" == json.optString("type", "")) {
|
|
@@ -3386,7 +3408,9 @@ class MentraLive : SGCManager() {
|
|
|
3386
3408
|
json,
|
|
3387
3409
|
jsonStr.length
|
|
3388
3410
|
)
|
|
3389
|
-
sendDataToGlasses(jsonStr, wakeup, sessionGeneration)
|
|
3411
|
+
val accepted = sendDataToGlasses(jsonStr, wakeup, sessionGeneration, bridgeLogging)
|
|
3412
|
+
if (!accepted) pendingMessages.remove(messageId)
|
|
3413
|
+
return accepted
|
|
3390
3414
|
}
|
|
3391
3415
|
} catch (e: JSONException) {
|
|
3392
3416
|
Log.e(TAG, "Error adding message ID to JSON", e)
|
|
@@ -3394,6 +3418,7 @@ class MentraLive : SGCManager() {
|
|
|
3394
3418
|
} else {
|
|
3395
3419
|
Bridge.log("LIVE: Cannot send JSON to ASG, JSON is null")
|
|
3396
3420
|
}
|
|
3421
|
+
return false
|
|
3397
3422
|
}
|
|
3398
3423
|
|
|
3399
3424
|
private fun sendJson(json: JSONObject?) {
|
|
@@ -3411,20 +3436,25 @@ class MentraLive : SGCManager() {
|
|
|
3411
3436
|
messageId: Long,
|
|
3412
3437
|
messageData: String,
|
|
3413
3438
|
timeoutMs: Long,
|
|
3414
|
-
sessionGeneration: Long
|
|
3439
|
+
sessionGeneration: Long,
|
|
3440
|
+
bridgeLogging: Boolean = true,
|
|
3415
3441
|
) {
|
|
3416
3442
|
if (!isConnected || sessionGeneration != bleSessionGeneration.get()) {
|
|
3417
|
-
|
|
3443
|
+
transportLog(
|
|
3444
|
+
"LIVE: Not connected, skipping ACK tracking for message " + messageId,
|
|
3445
|
+
bridgeLogging,
|
|
3446
|
+
)
|
|
3418
3447
|
return
|
|
3419
3448
|
}
|
|
3420
3449
|
|
|
3421
3450
|
// Skip ACK tracking for glasses with build number < 5 (older firmware)
|
|
3422
3451
|
if (buildNumberInt < 5) {
|
|
3423
|
-
|
|
3452
|
+
transportLog(
|
|
3424
3453
|
"LIVE: Glasses build number (" +
|
|
3425
3454
|
buildNumberInt +
|
|
3426
3455
|
") < 5, skipping ACK tracking for message " +
|
|
3427
|
-
messageId
|
|
3456
|
+
messageId,
|
|
3457
|
+
bridgeLogging,
|
|
3428
3458
|
)
|
|
3429
3459
|
return
|
|
3430
3460
|
}
|
|
@@ -3446,11 +3476,20 @@ class MentraLive : SGCManager() {
|
|
|
3446
3476
|
// Schedule ACK timeout with custom timeout
|
|
3447
3477
|
handler.postDelayed({ checkMessageAck(messageId, sessionGeneration) }, timeoutMs)
|
|
3448
3478
|
|
|
3449
|
-
|
|
3450
|
-
"LIVE: 📋 Tracking message " + messageId + " for ACK (timeout: " + timeoutMs + "ms)"
|
|
3479
|
+
transportLog(
|
|
3480
|
+
"LIVE: 📋 Tracking message " + messageId + " for ACK (timeout: " + timeoutMs + "ms)",
|
|
3481
|
+
bridgeLogging,
|
|
3451
3482
|
)
|
|
3452
3483
|
}
|
|
3453
3484
|
|
|
3485
|
+
private fun transportLog(message: String, bridgeLogging: Boolean) {
|
|
3486
|
+
if (bridgeLogging) {
|
|
3487
|
+
Bridge.log(message)
|
|
3488
|
+
} else {
|
|
3489
|
+
Log.d(TAG, message)
|
|
3490
|
+
}
|
|
3491
|
+
}
|
|
3492
|
+
|
|
3454
3493
|
/**
|
|
3455
3494
|
* Ends the current BLE send session before clearing its ACK state. Scheduled callbacks capture
|
|
3456
3495
|
* the old generation, and queued retries retain it, so a retry racing this clear cannot become
|
|
@@ -3944,6 +3983,9 @@ class MentraLive : SGCManager() {
|
|
|
3944
3983
|
}
|
|
3945
3984
|
}
|
|
3946
3985
|
|
|
3986
|
+
val incomingMessageId = if (json.has("mId")) json.optLong("mId") else null
|
|
3987
|
+
incomingGlassesMessageAckId(type, incomingMessageId)?.let(::sendAckToGlasses)
|
|
3988
|
+
|
|
3947
3989
|
// Check if this is a K900 command format (has "C" field instead of "type")
|
|
3948
3990
|
if (json.has("C")) {
|
|
3949
3991
|
processK900JsonMessage(json)
|
|
@@ -4044,6 +4086,14 @@ class MentraLive : SGCManager() {
|
|
|
4044
4086
|
val percent = json.optInt("percent", batteryLevel)
|
|
4045
4087
|
updateBatteryStatus(percent, isCharging)
|
|
4046
4088
|
}
|
|
4089
|
+
"stream_controller_probe" -> {
|
|
4090
|
+
val values = mapOf("protocolVersion" to json.opt("protocolVersion"),
|
|
4091
|
+
"controllerId" to json.opt("controllerId"), "streamId" to json.opt("streamId"),
|
|
4092
|
+
"probeId" to json.opt("probeId"))
|
|
4093
|
+
com.mentra.bluetoothsdk.streaming.StreamControllerProbe.response(values)?.let {
|
|
4094
|
+
sendJson(JSONObject(it))
|
|
4095
|
+
}
|
|
4096
|
+
}
|
|
4047
4097
|
"pong" ->
|
|
4048
4098
|
// Process heartbeat pong response
|
|
4049
4099
|
Bridge.log("LIVE: Received pong response - connection healthy")
|
|
@@ -4098,6 +4148,44 @@ class MentraLive : SGCManager() {
|
|
|
4098
4148
|
wifiError.takeIf { it.isNotEmpty() }
|
|
4099
4149
|
)
|
|
4100
4150
|
}
|
|
4151
|
+
"wifi_forget_result" -> {
|
|
4152
|
+
if (!wifiResponseEnvelopeIsValid(json.keys().asSequence().associateWith { json.get(it) }, allowLegacy = true)) return
|
|
4153
|
+
Bridge.sendWifiForgetResult(
|
|
4154
|
+
requestId = json.optString("requestId", ""),
|
|
4155
|
+
sid = json.optString("sid", ""),
|
|
4156
|
+
ssid = json.optString("ssid", ""),
|
|
4157
|
+
protocolVersion = json.optInt("protocol_version", 0),
|
|
4158
|
+
outcome = json.optString("outcome", ""),
|
|
4159
|
+
legacyDispatched =
|
|
4160
|
+
if (json.has("dispatched")) json.optBoolean("dispatched") else null,
|
|
4161
|
+
connected =
|
|
4162
|
+
if (json.has("connected")) json.optBoolean("connected") else null,
|
|
4163
|
+
currentSsid = json.optString("current_ssid", ""),
|
|
4164
|
+
localIp = json.optString("local_ip", ""),
|
|
4165
|
+
error = json.optString("error", "").ifEmpty { null },
|
|
4166
|
+
)
|
|
4167
|
+
}
|
|
4168
|
+
"saved_wifi_networks" -> {
|
|
4169
|
+
if (!wifiResponseEnvelopeIsValid(json.keys().asSequence().associateWith { json.get(it) }, allowLegacy = false)) return
|
|
4170
|
+
val networks = mutableListOf<String>()
|
|
4171
|
+
val networkArray = json.optJSONArray("networks") ?: return
|
|
4172
|
+
if (networkArray != null) {
|
|
4173
|
+
for (i in 0 until networkArray.length()) {
|
|
4174
|
+
val network = networkArray.opt(i) as? String ?: return
|
|
4175
|
+
network.takeIf { it.trim().isNotEmpty() }?.let {
|
|
4176
|
+
networks.add(it)
|
|
4177
|
+
}
|
|
4178
|
+
}
|
|
4179
|
+
}
|
|
4180
|
+
Bridge.sendSavedWifiNetworks(
|
|
4181
|
+
requestId = json.optString("requestId", ""),
|
|
4182
|
+
sid = json.optString("sid", ""),
|
|
4183
|
+
protocolVersion = json.optInt("protocol_version", 0),
|
|
4184
|
+
outcome = json.optString("outcome", ""),
|
|
4185
|
+
networks = networks,
|
|
4186
|
+
error = json.optString("error", "").ifEmpty { null },
|
|
4187
|
+
)
|
|
4188
|
+
}
|
|
4101
4189
|
"hotspot_status_update" -> {
|
|
4102
4190
|
// Process hotspot status information (same pattern as "wifi_status")
|
|
4103
4191
|
val hotspotEnabled = json.optBoolean("hotspot_enabled", false)
|
|
@@ -4479,8 +4567,16 @@ class MentraLive : SGCManager() {
|
|
|
4479
4567
|
// Record the session id BEFORE marking ready: an unsolicited glasses_ready
|
|
4480
4568
|
// already runs this full remote-reset flow, so recording (not re-triggering)
|
|
4481
4569
|
// is correct here; version_info detection covers the restart case.
|
|
4482
|
-
json.optString("sid", "").takeIf { it.isNotEmpty() }
|
|
4570
|
+
glassesSessionId = json.optString("sid", "").takeIf { it.isNotEmpty() }
|
|
4571
|
+
Bridge.sendTypedMessage(
|
|
4572
|
+
"wifi_protocol_session_ready",
|
|
4573
|
+
mapOf("sid" to (glassesSessionId ?: "")),
|
|
4574
|
+
)
|
|
4483
4575
|
readinessCompletedThisBleSession = true
|
|
4576
|
+
Bridge.sendTypedMessage("stream_control_ready", mapOf(
|
|
4577
|
+
"sid" to json.optString("sid", ""),
|
|
4578
|
+
"streamControlVersion" to json.optInt("streamControlVersion", 0),
|
|
4579
|
+
))
|
|
4484
4580
|
|
|
4485
4581
|
// Set the ready flag to stop any future readiness checks
|
|
4486
4582
|
glassesReady = true
|
|
@@ -4673,6 +4769,7 @@ class MentraLive : SGCManager() {
|
|
|
4673
4769
|
}
|
|
4674
4770
|
|
|
4675
4771
|
val versionInfoLegacy = HashMap<String, Any>()
|
|
4772
|
+
versionInfoLegacy["version_info_type"] = "version_info"
|
|
4676
4773
|
versionInfoLegacy["appVersion"] = appVersionLegacy
|
|
4677
4774
|
versionInfoLegacy["buildNumber"] = buildNumberLegacy
|
|
4678
4775
|
versionInfoLegacy["deviceModel"] = deviceModelLegacy
|
|
@@ -4856,6 +4953,7 @@ class MentraLive : SGCManager() {
|
|
|
4856
4953
|
|
|
4857
4954
|
// Extract all fields from JSON (except "type")
|
|
4858
4955
|
val fields = HashMap<String, Any>()
|
|
4956
|
+
fields["version_info_type"] = type
|
|
4859
4957
|
val keys = json.keys()
|
|
4860
4958
|
while (keys.hasNext()) {
|
|
4861
4959
|
val key = keys.next()
|
|
@@ -6716,6 +6814,8 @@ class MentraLive : SGCManager() {
|
|
|
6716
6814
|
val json = JSONObject(message as Map<*, *>)
|
|
6717
6815
|
// Remove timestamp as iOS does
|
|
6718
6816
|
json.remove("timestamp")
|
|
6817
|
+
json.put("controllerProbeVersion", 1)
|
|
6818
|
+
json.put("controllerId", com.mentra.bluetoothsdk.streaming.StreamControllerProbe.controllerId)
|
|
6719
6819
|
sendJson(json, true)
|
|
6720
6820
|
} catch (e: Exception) {
|
|
6721
6821
|
Log.e(TAG, "Error creating RTMP stream start JSON", e)
|
|
@@ -6978,7 +7078,16 @@ class MentraLive : SGCManager() {
|
|
|
6978
7078
|
"ctkd_bond_none",
|
|
6979
7079
|
"prev=$previousBondState queueSize=${sendQueue.size}"
|
|
6980
7080
|
)
|
|
6981
|
-
|
|
7081
|
+
if (previousBondState == BluetoothDevice.BOND_BONDED) {
|
|
7082
|
+
// A real unpair ends this Classic session. Invalidate
|
|
7083
|
+
// the target so delayed profile broadcasts cannot
|
|
7084
|
+
// make pairing look connected again.
|
|
7085
|
+
classicAudioConnectionTracker.invalidate(device.address)
|
|
7086
|
+
} else {
|
|
7087
|
+
// A failed/cancelled bond may be retried below, so keep
|
|
7088
|
+
// the target while clearing any partial profile state.
|
|
7089
|
+
classicAudioConnectionTracker.clear(device.address)
|
|
7090
|
+
}
|
|
6982
7091
|
audioConnected = false
|
|
6983
7092
|
if (previousBondState == BluetoothDevice.BOND_BONDING) {
|
|
6984
7093
|
// User cancelled or bonding failed - retry up to
|
|
@@ -7078,6 +7187,124 @@ class MentraLive : SGCManager() {
|
|
|
7078
7187
|
}
|
|
7079
7188
|
}
|
|
7080
7189
|
|
|
7190
|
+
/** Observe the two Android Classic audio profiles that Mentra Live exposes. */
|
|
7191
|
+
private fun initializeClassicAudioReceiver() {
|
|
7192
|
+
classicAudioReceiver =
|
|
7193
|
+
object : BroadcastReceiver() {
|
|
7194
|
+
override fun onReceive(context: Context?, intent: Intent?) {
|
|
7195
|
+
if (intent?.action != BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED &&
|
|
7196
|
+
intent?.action != BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED) return
|
|
7197
|
+
val device = intent.bluetoothDeviceExtra() ?: return
|
|
7198
|
+
refreshClassicAudioConnectionState(device)
|
|
7199
|
+
}
|
|
7200
|
+
}
|
|
7201
|
+
}
|
|
7202
|
+
|
|
7203
|
+
private fun registerClassicAudioReceiver() {
|
|
7204
|
+
val ctx = context ?: return
|
|
7205
|
+
val receiver = classicAudioReceiver ?: return
|
|
7206
|
+
if (isClassicAudioReceiverRegistered) return
|
|
7207
|
+
|
|
7208
|
+
try {
|
|
7209
|
+
val filter =
|
|
7210
|
+
IntentFilter().apply {
|
|
7211
|
+
addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED)
|
|
7212
|
+
addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)
|
|
7213
|
+
}
|
|
7214
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
7215
|
+
ctx.registerReceiver(receiver, filter, Context.RECEIVER_EXPORTED)
|
|
7216
|
+
} else {
|
|
7217
|
+
ctx.registerReceiver(receiver, filter)
|
|
7218
|
+
}
|
|
7219
|
+
isClassicAudioReceiverRegistered = true
|
|
7220
|
+
Bridge.log("LIVE: Classic: A2DP/HFP status receiver registered")
|
|
7221
|
+
} catch (e: Exception) {
|
|
7222
|
+
Bridge.log("LIVE: Classic: failed to register status receiver: " + e.message)
|
|
7223
|
+
}
|
|
7224
|
+
}
|
|
7225
|
+
|
|
7226
|
+
private fun unregisterClassicAudioReceiver() {
|
|
7227
|
+
if (!isClassicAudioReceiverRegistered || classicAudioReceiver == null) return
|
|
7228
|
+
|
|
7229
|
+
try {
|
|
7230
|
+
context?.unregisterReceiver(classicAudioReceiver)
|
|
7231
|
+
} catch (e: Exception) {
|
|
7232
|
+
Bridge.log("LIVE: Classic: failed to unregister status receiver: " + e.message)
|
|
7233
|
+
} finally {
|
|
7234
|
+
isClassicAudioReceiverRegistered = false
|
|
7235
|
+
}
|
|
7236
|
+
}
|
|
7237
|
+
|
|
7238
|
+
/**
|
|
7239
|
+
* Broadcast payloads are hints, never current truth (even when the MAC matches). Query both
|
|
7240
|
+
* profiles and publish one combined snapshot. A newer refresh or teardown invalidates all
|
|
7241
|
+
* outstanding results, including a previous connection to the same physical glasses.
|
|
7242
|
+
*/
|
|
7243
|
+
private fun refreshClassicAudioConnectionState(device: BluetoothDevice) {
|
|
7244
|
+
if (Looper.myLooper() != handler.looper) {
|
|
7245
|
+
handler.post { refreshClassicAudioConnectionState(device) }
|
|
7246
|
+
return
|
|
7247
|
+
}
|
|
7248
|
+
if (isKilled) return
|
|
7249
|
+
val adapter = bluetoothAdapter ?: return
|
|
7250
|
+
val ctx = context ?: return
|
|
7251
|
+
val ticket = classicAudioConnectionTracker.beginSnapshot(device.address) ?: return
|
|
7252
|
+
val states = mutableMapOf<ClassicAudioProfile, Boolean>()
|
|
7253
|
+
for ((profileId, audioProfile) in listOf(
|
|
7254
|
+
BluetoothProfile.A2DP to ClassicAudioProfile.A2DP,
|
|
7255
|
+
BluetoothProfile.HEADSET to ClassicAudioProfile.HEADSET,
|
|
7256
|
+
)) {
|
|
7257
|
+
try {
|
|
7258
|
+
val requested = adapter.getProfileProxy(ctx, object : BluetoothProfile.ServiceListener {
|
|
7259
|
+
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
|
|
7260
|
+
// Close before dispatch: destroy() may discard queued handler work.
|
|
7261
|
+
val connectedState = try {
|
|
7262
|
+
if (profile != profileId) null
|
|
7263
|
+
else proxy.getConnectionState(device) == BluetoothProfile.STATE_CONNECTED
|
|
7264
|
+
} catch (e: Exception) {
|
|
7265
|
+
Bridge.log("LIVE: Classic: snapshot unavailable: " + e.message)
|
|
7266
|
+
null
|
|
7267
|
+
} finally {
|
|
7268
|
+
try { adapter.closeProfileProxy(profileId, proxy) }
|
|
7269
|
+
catch (_: Exception) {}
|
|
7270
|
+
}
|
|
7271
|
+
if (connectedState == null) return
|
|
7272
|
+
handler.post {
|
|
7273
|
+
if (isKilled || profile != profileId) return@post
|
|
7274
|
+
states[audioProfile] = connectedState
|
|
7275
|
+
val connected = readyClassicAudioSnapshot(states) ?: return@post
|
|
7276
|
+
classicAudioConnectionTracker.applySnapshot(
|
|
7277
|
+
ticket, device.address, connected) {
|
|
7278
|
+
if (ClassicAudioProfile.A2DP in connected) {
|
|
7279
|
+
markAudioConnected(device)
|
|
7280
|
+
} else if (states[ClassicAudioProfile.A2DP] == false && audioConnected) {
|
|
7281
|
+
audioConnected = false
|
|
7282
|
+
Bridge.sendAudioDisconnected()
|
|
7283
|
+
}
|
|
7284
|
+
}
|
|
7285
|
+
}
|
|
7286
|
+
}
|
|
7287
|
+
override fun onServiceDisconnected(profile: Int) {}
|
|
7288
|
+
}, profileId)
|
|
7289
|
+
if (!requested) {
|
|
7290
|
+
// Unknown is not disconnected. A successful other-profile read can still
|
|
7291
|
+
// publish connected without waiting for this unavailable service.
|
|
7292
|
+
Bridge.log("LIVE: Classic: profile service unavailable: $profileId")
|
|
7293
|
+
}
|
|
7294
|
+
} catch (e: Exception) {
|
|
7295
|
+
Bridge.log("LIVE: Classic: profile query unavailable: " + e.message)
|
|
7296
|
+
}
|
|
7297
|
+
}
|
|
7298
|
+
}
|
|
7299
|
+
|
|
7300
|
+
@Suppress("DEPRECATION")
|
|
7301
|
+
private fun Intent.bluetoothDeviceExtra(): BluetoothDevice? =
|
|
7302
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
7303
|
+
getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)
|
|
7304
|
+
} else {
|
|
7305
|
+
getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
|
|
7306
|
+
}
|
|
7307
|
+
|
|
7081
7308
|
/**
|
|
7082
7309
|
* Start CTKD Classic bonding / A2DP only after BLE notifications are ready. Calling this from
|
|
7083
7310
|
* onConnectionStateChange(CONNECTED) races dual-mode stacks and frequently yields GATT 19
|
|
@@ -7154,7 +7381,7 @@ class MentraLive : SGCManager() {
|
|
|
7154
7381
|
val method = device.javaClass.getMethod("removeBond")
|
|
7155
7382
|
val result = method.invoke(device) as Boolean
|
|
7156
7383
|
Bridge.log("LIVE: CTKD: Bond removal initiated, result: " + result)
|
|
7157
|
-
|
|
7384
|
+
classicAudioConnectionTracker.invalidate(device.address)
|
|
7158
7385
|
return result
|
|
7159
7386
|
} catch (e: Exception) {
|
|
7160
7387
|
Bridge.log("LIVE: CTKD: Error removing bond: " + e.message)
|
|
@@ -7164,7 +7391,7 @@ class MentraLive : SGCManager() {
|
|
|
7164
7391
|
|
|
7165
7392
|
/** Check if BT Classic is connected via CTKD */
|
|
7166
7393
|
fun isBtClassicConnected(): Boolean {
|
|
7167
|
-
return
|
|
7394
|
+
return classicAudioConnectionTracker.connected
|
|
7168
7395
|
}
|
|
7169
7396
|
|
|
7170
7397
|
/** A2DP profile service listener for connecting to already-bonded devices */
|
|
@@ -7223,11 +7450,11 @@ class MentraLive : SGCManager() {
|
|
|
7223
7450
|
try {
|
|
7224
7451
|
val state = a2dpProfile!!.getConnectionState(device)
|
|
7225
7452
|
Bridge.log("LIVE: A2DP: Current connection state: " + state)
|
|
7453
|
+
refreshClassicAudioConnectionState(device)
|
|
7226
7454
|
|
|
7227
7455
|
if (state == BluetoothProfile.STATE_CONNECTED) {
|
|
7228
7456
|
Bridge.log("LIVE: A2DP: Already connected to " + device.name)
|
|
7229
7457
|
a2dpConnectAttempts = 0
|
|
7230
|
-
markAudioConnected(device.name)
|
|
7231
7458
|
} else if (state == BluetoothProfile.STATE_DISCONNECTED) {
|
|
7232
7459
|
Bridge.log("LIVE: A2DP: Connecting to " + device.name)
|
|
7233
7460
|
// Use reflection to call connect() as it's a hidden API
|
|
@@ -7280,16 +7507,18 @@ class MentraLive : SGCManager() {
|
|
|
7280
7507
|
}
|
|
7281
7508
|
|
|
7282
7509
|
/** Helper to mark audio as connected and notify */
|
|
7283
|
-
private fun markAudioConnected(
|
|
7510
|
+
private fun markAudioConnected(device: BluetoothDevice) {
|
|
7284
7511
|
if (isKilled) {
|
|
7285
7512
|
Bridge.log(
|
|
7286
7513
|
"LIVE: A2DP: Ignoring markAudioConnected — SGC destroyed (would confuse DeviceManager)"
|
|
7287
7514
|
)
|
|
7288
7515
|
return
|
|
7289
7516
|
}
|
|
7290
|
-
|
|
7517
|
+
val wasAudioConnected = audioConnected
|
|
7291
7518
|
audioConnected = true
|
|
7292
|
-
|
|
7519
|
+
if (!wasAudioConnected) {
|
|
7520
|
+
Bridge.sendAudioConnected(safeDeviceName(device))
|
|
7521
|
+
}
|
|
7293
7522
|
if (glassesReadyReceived) {
|
|
7294
7523
|
Bridge.log(
|
|
7295
7524
|
"LIVE: A2DP: Both audio and glasses_ready confirmed - marking as fully connected"
|
|
@@ -7372,10 +7601,10 @@ class MentraLive : SGCManager() {
|
|
|
7372
7601
|
device.address +
|
|
7373
7602
|
")"
|
|
7374
7603
|
)
|
|
7375
|
-
|
|
7376
|
-
|
|
7377
|
-
|
|
7378
|
-
|
|
7604
|
+
// The BLE device reference may already be cleared after a GATT drop. The tracker owns the
|
|
7605
|
+
// authoritative target address, so let it decide whether this teardown belongs to the
|
|
7606
|
+
// active Classic session instead of consulting connectedDevice.
|
|
7607
|
+
if (classicAudioConnectionTracker.invalidate(device.address)) {
|
|
7379
7608
|
audioConnected = false
|
|
7380
7609
|
}
|
|
7381
7610
|
|
|
@@ -7507,12 +7736,15 @@ class MentraLive : SGCManager() {
|
|
|
7507
7736
|
|
|
7508
7737
|
// CTKD Implementation: Unregister bonding receiver
|
|
7509
7738
|
unregisterBondingReceiver()
|
|
7739
|
+
unregisterClassicAudioReceiver()
|
|
7510
7740
|
|
|
7511
7741
|
// Tear down Classic (A2DP/HFP) before GATT close. Closing only the proxy left the
|
|
7512
7742
|
// phone's system Bluetooth UI showing Mentra Live still connected.
|
|
7513
7743
|
val classicDevice = connectedDevice
|
|
7514
7744
|
disconnectClassicProfiles(classicDevice)
|
|
7515
7745
|
closeA2dpProxy()
|
|
7746
|
+
classicAudioConnectionTracker.reset()
|
|
7747
|
+
DeviceStore.apply("glasses", "bluetoothClassicConnected", false)
|
|
7516
7748
|
|
|
7517
7749
|
// Stop readiness check loop
|
|
7518
7750
|
stopReadinessCheckLoop()
|
|
@@ -8867,9 +9099,10 @@ class MentraLive : SGCManager() {
|
|
|
8867
9099
|
wireBytes: Int,
|
|
8868
9100
|
packetCount: Int,
|
|
8869
9101
|
protocolVersion: Int,
|
|
8870
|
-
direction: String
|
|
9102
|
+
direction: String,
|
|
9103
|
+
bridgeLogging: Boolean = true,
|
|
8871
9104
|
) {
|
|
8872
|
-
|
|
9105
|
+
transportLog(
|
|
8873
9106
|
"BLE_TRACE direction=" +
|
|
8874
9107
|
direction +
|
|
8875
9108
|
" proto=v" +
|
|
@@ -8879,7 +9112,8 @@ class MentraLive : SGCManager() {
|
|
|
8879
9112
|
" wire=" +
|
|
8880
9113
|
wireBytes +
|
|
8881
9114
|
" packets=" +
|
|
8882
|
-
packetCount
|
|
9115
|
+
packetCount,
|
|
9116
|
+
bridgeLogging,
|
|
8883
9117
|
)
|
|
8884
9118
|
}
|
|
8885
9119
|
|
|
@@ -8897,15 +9131,19 @@ class MentraLive : SGCManager() {
|
|
|
8897
9131
|
private fun sendDataToGlasses(
|
|
8898
9132
|
data: String?,
|
|
8899
9133
|
wakeup: Boolean,
|
|
8900
|
-
sessionGeneration: Long
|
|
8901
|
-
|
|
9134
|
+
sessionGeneration: Long,
|
|
9135
|
+
bridgeLogging: Boolean = true,
|
|
9136
|
+
): Boolean {
|
|
8902
9137
|
if (data == null || data.isEmpty()) {
|
|
8903
9138
|
Log.e(TAG, "Cannot send empty data to glasses")
|
|
8904
|
-
return
|
|
9139
|
+
return false
|
|
8905
9140
|
}
|
|
8906
9141
|
if (sessionGeneration != bleSessionGeneration.get()) {
|
|
8907
|
-
|
|
8908
|
-
|
|
9142
|
+
transportLog(
|
|
9143
|
+
"LIVE: Dropping send request from stale BLE session $sessionGeneration",
|
|
9144
|
+
bridgeLogging,
|
|
9145
|
+
)
|
|
9146
|
+
return false
|
|
8909
9147
|
}
|
|
8910
9148
|
|
|
8911
9149
|
val wireData =
|
|
@@ -8933,14 +9171,14 @@ class MentraLive : SGCManager() {
|
|
|
8933
9171
|
}
|
|
8934
9172
|
|
|
8935
9173
|
if (useBinaryWireProtocol && buildNumberInt >= 5) {
|
|
8936
|
-
sendDataToGlassesBinary(
|
|
9174
|
+
return sendDataToGlassesBinary(
|
|
8937
9175
|
wireData,
|
|
8938
9176
|
wakeup,
|
|
8939
9177
|
commandTraceInfo,
|
|
8940
9178
|
isPhotoRequest,
|
|
8941
|
-
sessionGeneration
|
|
9179
|
+
sessionGeneration,
|
|
9180
|
+
bridgeLogging,
|
|
8942
9181
|
)
|
|
8943
|
-
return
|
|
8944
9182
|
}
|
|
8945
9183
|
|
|
8946
9184
|
// First check if the message needs chunking
|
|
@@ -8954,7 +9192,7 @@ class MentraLive : SGCManager() {
|
|
|
8954
9192
|
|
|
8955
9193
|
// Check if chunking is needed
|
|
8956
9194
|
if (MessageChunker.needsChunking(testWrappedJson)) {
|
|
8957
|
-
|
|
9195
|
+
transportLog("LIVE: Message exceeds threshold, chunking required", bridgeLogging)
|
|
8958
9196
|
if (isPhotoRequest) {
|
|
8959
9197
|
Bridge.log(
|
|
8960
9198
|
"LIVE: PHOTO PIPELINE BLE handoff — chunking enabled for request payload"
|
|
@@ -8972,7 +9210,8 @@ class MentraLive : SGCManager() {
|
|
|
8972
9210
|
|
|
8973
9211
|
// Create chunks
|
|
8974
9212
|
val chunks = MessageChunker.createChunks(wireData, messageId, wakeup)
|
|
8975
|
-
|
|
9213
|
+
if (chunks.isEmpty()) return false
|
|
9214
|
+
transportLog("LIVE: Sending " + chunks.size + " chunks", bridgeLogging)
|
|
8976
9215
|
if (isPhotoRequest) {
|
|
8977
9216
|
Bridge.log(
|
|
8978
9217
|
"LIVE: PHOTO PIPELINE BLE handoff — created " +
|
|
@@ -9030,13 +9269,13 @@ class MentraLive : SGCManager() {
|
|
|
9030
9269
|
}
|
|
9031
9270
|
}
|
|
9032
9271
|
|
|
9033
|
-
|
|
9272
|
+
transportLog("LIVE: All chunks queued for transmission", bridgeLogging)
|
|
9034
9273
|
if (isPhotoRequest) {
|
|
9035
9274
|
Bridge.log("LIVE: PHOTO PIPELINE BLE handoff — all photo chunks queued")
|
|
9036
9275
|
}
|
|
9037
9276
|
} else {
|
|
9038
9277
|
// Normal single message transmission
|
|
9039
|
-
|
|
9278
|
+
transportLog("LIVE: Sending data to glasses: " + wireData, bridgeLogging)
|
|
9040
9279
|
|
|
9041
9280
|
// Pack the data using the centralized utility with the negotiated endianness
|
|
9042
9281
|
val packedData =
|
|
@@ -9070,7 +9309,9 @@ class MentraLive : SGCManager() {
|
|
|
9070
9309
|
}
|
|
9071
9310
|
} catch (e: Exception) {
|
|
9072
9311
|
Log.e(TAG, "Error creating data JSON", e)
|
|
9312
|
+
return false
|
|
9073
9313
|
}
|
|
9314
|
+
return sessionGeneration == bleSessionGeneration.get()
|
|
9074
9315
|
}
|
|
9075
9316
|
|
|
9076
9317
|
private fun sendDataToGlassesBinary(
|
|
@@ -9078,8 +9319,9 @@ class MentraLive : SGCManager() {
|
|
|
9078
9319
|
wakeup: Boolean,
|
|
9079
9320
|
commandTraceInfo: OutgoingBleCommandTraceInfo,
|
|
9080
9321
|
isPhotoRequest: Boolean,
|
|
9081
|
-
sessionGeneration: Long
|
|
9082
|
-
|
|
9322
|
+
sessionGeneration: Long,
|
|
9323
|
+
bridgeLogging: Boolean,
|
|
9324
|
+
): Boolean {
|
|
9083
9325
|
val payloadBytes = data.toByteArray(StandardCharsets.UTF_8)
|
|
9084
9326
|
var messageId = 0
|
|
9085
9327
|
var ackRequested = false
|
|
@@ -9098,6 +9340,7 @@ class MentraLive : SGCManager() {
|
|
|
9098
9340
|
wakeup,
|
|
9099
9341
|
ackRequested
|
|
9100
9342
|
)
|
|
9343
|
+
if (fragments.isEmpty()) return false
|
|
9101
9344
|
var totalWireBytes = 0
|
|
9102
9345
|
for (i in fragments.indices) {
|
|
9103
9346
|
val fragment = fragments[i]
|
|
@@ -9146,7 +9389,8 @@ class MentraLive : SGCManager() {
|
|
|
9146
9389
|
totalWireBytes,
|
|
9147
9390
|
fragments.size,
|
|
9148
9391
|
BleWireProtocol.PROTOCOL_V2,
|
|
9149
|
-
"phone_to_glasses"
|
|
9392
|
+
"phone_to_glasses",
|
|
9393
|
+
bridgeLogging,
|
|
9150
9394
|
)
|
|
9151
9395
|
|
|
9152
9396
|
if (isPhotoRequest) {
|
|
@@ -9157,6 +9401,7 @@ class MentraLive : SGCManager() {
|
|
|
9157
9401
|
totalWireBytes
|
|
9158
9402
|
)
|
|
9159
9403
|
}
|
|
9404
|
+
return sessionGeneration == bleSessionGeneration.get()
|
|
9160
9405
|
}
|
|
9161
9406
|
|
|
9162
9407
|
private fun parseOutgoingBleCommandTraceInfo(payload: String): OutgoingBleCommandTraceInfo {
|
|
@@ -9438,16 +9683,45 @@ class MentraLive : SGCManager() {
|
|
|
9438
9683
|
* K900 SystemUI can properly clear the cached credentials
|
|
9439
9684
|
*/
|
|
9440
9685
|
override fun forgetWifiNetwork(ssid: String) {
|
|
9441
|
-
|
|
9686
|
+
forgetWifiNetwork(ssid, null, null)
|
|
9687
|
+
}
|
|
9688
|
+
|
|
9689
|
+
override fun forgetWifiNetwork(ssid: String, requestId: String?, sid: String?): Boolean {
|
|
9690
|
+
if (!isConnected || bluetoothGatt == null || txCharacteristic == null || ssid.isBlank()) return false
|
|
9691
|
+
if ((requestId == null) != (sid == null)) return false
|
|
9692
|
+
if (requestId != null && (requestId.isBlank() || sid.isNullOrBlank())) return false
|
|
9693
|
+
Log.d(TAG, "LIVE: 📶 Sending WiFi forget command for SSID: " + ssid)
|
|
9442
9694
|
|
|
9443
9695
|
try {
|
|
9444
9696
|
val wifiCommand = JSONObject()
|
|
9445
9697
|
wifiCommand.put("type", "forget_wifi")
|
|
9446
9698
|
wifiCommand.put("ssid", ssid)
|
|
9447
|
-
|
|
9699
|
+
if (!requestId.isNullOrEmpty()) {
|
|
9700
|
+
wifiCommand.put("requestId", requestId)
|
|
9701
|
+
}
|
|
9702
|
+
if (!sid.isNullOrEmpty()) {
|
|
9703
|
+
wifiCommand.put("sid", sid)
|
|
9704
|
+
wifiCommand.put("protocolVersion", 1)
|
|
9705
|
+
}
|
|
9706
|
+
// This dispatch runs under the WiFi lifecycle lock. Keep its transport logs
|
|
9707
|
+
// native-only so a public log listener cannot re-enter reset before it is queued.
|
|
9708
|
+
return sendJson(wifiCommand, true, bridgeLogging = false)
|
|
9448
9709
|
} catch (e: JSONException) {
|
|
9449
9710
|
Log.e(TAG, "Error creating WiFi forget JSON", e)
|
|
9450
9711
|
}
|
|
9712
|
+
return false
|
|
9713
|
+
}
|
|
9714
|
+
|
|
9715
|
+
override fun requestSavedWifiNetworks(requestId: String, sid: String): Boolean {
|
|
9716
|
+
if (!isConnected || bluetoothGatt == null || txCharacteristic == null || requestId.isBlank() || sid.isBlank()) return false
|
|
9717
|
+
val command = JSONObject()
|
|
9718
|
+
command.put("type", "request_saved_wifi_networks")
|
|
9719
|
+
command.put("requestId", requestId)
|
|
9720
|
+
command.put("protocolVersion", 1)
|
|
9721
|
+
if (sid.isNotEmpty()) {
|
|
9722
|
+
command.put("sid", sid)
|
|
9723
|
+
}
|
|
9724
|
+
return sendJson(command, true, bridgeLogging = false)
|
|
9451
9725
|
}
|
|
9452
9726
|
|
|
9453
9727
|
override fun sendHotspotState(enabled: Boolean) {
|
|
@@ -9554,6 +9828,13 @@ class MentraLive : SGCManager() {
|
|
|
9554
9828
|
}
|
|
9555
9829
|
}
|
|
9556
9830
|
|
|
9831
|
+
private fun sendAckToGlasses(messageId: Long) {
|
|
9832
|
+
val ack = JSONObject()
|
|
9833
|
+
ack.put("type", "msg_ack")
|
|
9834
|
+
ack.put("mId", messageId)
|
|
9835
|
+
sendJsonWithoutAck(ack, false)
|
|
9836
|
+
}
|
|
9837
|
+
|
|
9557
9838
|
private fun sendJsonWithoutAck(json: JSONObject?) {
|
|
9558
9839
|
sendJsonWithoutAck(json, false)
|
|
9559
9840
|
}
|