@mentra/bluetooth-sdk 3.4.0-dev.993 → 3.4.0-dev.998
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/MIGRATION.md +61 -0
- package/README.md +47 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +6 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +8 -1
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +48 -4
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +21 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +2 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/connection/ConnectionModels.kt +5 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/GlassesAudioSetupPolicy.kt +64 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.kt +788 -134
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLiveOwnerLossPolicy.kt +227 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingAdvertisement.kt +19 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingReadinessPolicy.kt +94 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingStateMachine.kt +411 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingStateStore.kt +124 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +50 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/DeviceManagerOwnerLossTest.kt +146 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/GlassesStatusAudioSetupTest.kt +51 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/GlassesStatusClassicAudioTest.kt +2 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/GlassesAudioSetupPolicyTest.kt +65 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveAudioSetupReadinessTest.kt +231 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLiveOwnerLossPolicyTest.kt +368 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingAdvertisementParserTest.kt +20 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingReadinessPolicyTest.kt +136 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingStateMachineTest.kt +346 -0
- package/build/BluetoothSdk.types.d.ts +101 -0
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js +1 -0
- package/build/_private/BluetoothSdkModule.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 -2
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/Bridge.swift +11 -3
- package/ios/Source/DeviceManager.swift +80 -6
- package/ios/Source/DeviceStore.swift +27 -0
- package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
- package/ios/Source/MentraBluetoothSDK.swift +10 -4
- package/ios/Source/errors/BluetoothSdkError.swift +5 -1
- package/ios/Source/sgcs/GlassesAudioSetupPolicy.swift +181 -0
- package/ios/Source/sgcs/MentraLive.swift +1053 -166
- package/ios/Source/sgcs/MentraLivePairingAdvertisement.swift +260 -1
- package/ios/Source/sgcs/MentraLivePairingStateMachine.swift +385 -0
- package/ios/Source/sgcs/MentraLivePairingStateStore.swift +121 -0
- package/ios/Source/sgcs/SGCManager.swift +7 -0
- package/ios/Source/sgcs/Simulated.swift +2 -0
- package/ios/Source/status/DeviceStatus.swift +35 -0
- package/ios/Source/types/DeviceModels.swift +8 -1
- package/ios/Source/utils/AudioSessionMonitor.swift +158 -5
- package/ios/Tests/BluetoothAudioRouteTests.swift +82 -0
- package/ios/Tests/GlassesAudioSetupPolicyTests.swift +191 -0
- package/ios/Tests/MentraLiveAudioSetupTests.swift +215 -0
- package/ios/Tests/MentraLiveConnectionOptionsTests.swift +55 -0
- package/ios/Tests/MentraLivePairingAdvertisementTests.swift +327 -0
- package/ios/Tests/MentraLivePairingStateMachineTests.swift +402 -0
- package/ios/Tests/MentraLiveSessionReadinessTests.swift +328 -26
- package/package.json +3 -2
- package/src/BluetoothSdk.types.ts +97 -0
- package/src/_private/BluetoothSdkModule.ts +1 -0
- package/src/generated/releaseMetadata.ts +5 -5
- package/src/index.ts +10 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.sgcs
|
|
2
|
+
|
|
3
|
+
import android.bluetooth.BluetoothDevice
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Pure predicates behind Mentra Live pairing-mode pauses and rejection detection (pairing spec
|
|
7
|
+
* "Ownership transfer v4"). [MentraLivePairingStateMachine] owns the state; this object holds
|
|
8
|
+
* the per-signal rules both use. Mirrors iOS `MentraLiveConnectionAttemptPolicy`.
|
|
9
|
+
*
|
|
10
|
+
* Outside a pairing window the glasses reject a phone they no longer bond with at connect time
|
|
11
|
+
* (HCI Authentication Failure). A single rejection is only a hint: a glasses reboot, a stale
|
|
12
|
+
* bond, or a radio hiccup can produce one. Only secure-pairing firmware rejects on purpose, and
|
|
13
|
+
* only a rejection that repeats across automatic reconnects ([OwnerRejectionStreak]) counts.
|
|
14
|
+
*/
|
|
15
|
+
internal object MentraLiveOwnerLossPolicy {
|
|
16
|
+
/** `entering_pairing_mode.window_ms` when the glasses omit it. */
|
|
17
|
+
const val DEFAULT_PAIRING_WINDOW_MS = 120_000L
|
|
18
|
+
|
|
19
|
+
/** Covers the glasses' bounded close deferral (30 s) after the window timer. */
|
|
20
|
+
const val PAIRING_RESUME_MARGIN_MS = 30_000L
|
|
21
|
+
|
|
22
|
+
/** A bond the glasses dropped surfaces within a few seconds of the reconnect link. */
|
|
23
|
+
const val BOND_REMOVED_AFTER_RECONNECT_MS = 15_000L
|
|
24
|
+
|
|
25
|
+
/** Consecutive automatic-reconnect rejections before this phone stands down. */
|
|
26
|
+
const val REJECTIONS_TO_STAND_DOWN = 3
|
|
27
|
+
|
|
28
|
+
/** The rejections must keep arriving for about this long, so a glasses reboot is not owner loss. */
|
|
29
|
+
const val MIN_REJECTION_SPAN_MS = 45_000L
|
|
30
|
+
|
|
31
|
+
/** A gap this long between rejections means they are unrelated, not one streak. */
|
|
32
|
+
const val REJECTION_STREAK_GAP_MS = 90_000L
|
|
33
|
+
|
|
34
|
+
/** One failed attempt can report twice (bond removed, then GATT disconnect). Count it once. */
|
|
35
|
+
const val SAME_ATTEMPT_MS = 2_000L
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* HCI 0x05 Authentication Failure / 0x0F Unacceptable BD_ADDR from a connect-time rejection.
|
|
39
|
+
* The glasses refuse the previous owner during a window with 0x14, which never counts.
|
|
40
|
+
*/
|
|
41
|
+
fun isOwnerRejectionStatus(status: Int): Boolean = status == 0x05 || status == 0x0F
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A connect-time rejection of saved secure glasses. Legacy firmware never rejects a
|
|
45
|
+
* non-owner on purpose, and during an explicit pairing attempt the user is pairing anew.
|
|
46
|
+
* This is one observation; [MentraLivePairingStateMachine.onAuthRejection] decides what it adds up to.
|
|
47
|
+
*/
|
|
48
|
+
fun isOwnerRejectionOnGattError(
|
|
49
|
+
status: Int,
|
|
50
|
+
manualPairing: Boolean,
|
|
51
|
+
secureFirmware: Boolean,
|
|
52
|
+
): Boolean = secureFirmware && !manualPairing && isOwnerRejectionStatus(status)
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Android deletes its BLE keys when the glasses answer an encryption request without them.
|
|
56
|
+
* Right after an automatic reconnect that can mean the glasses forgot this phone. Like
|
|
57
|
+
* [isOwnerRejectionOnGattError] this is one observation, not yet owner loss.
|
|
58
|
+
*/
|
|
59
|
+
fun isOwnerRejectionOnBondRemoved(
|
|
60
|
+
previousBondState: Int,
|
|
61
|
+
bondState: Int,
|
|
62
|
+
killed: Boolean,
|
|
63
|
+
unpairInProgress: Boolean,
|
|
64
|
+
manualPairing: Boolean,
|
|
65
|
+
msSinceAutoReconnectLink: Long?,
|
|
66
|
+
secureFirmware: Boolean,
|
|
67
|
+
): Boolean {
|
|
68
|
+
if (!secureFirmware || killed || unpairInProgress || manualPairing) return false
|
|
69
|
+
if (previousBondState != BluetoothDevice.BOND_BONDED ||
|
|
70
|
+
bondState != BluetoothDevice.BOND_NONE
|
|
71
|
+
) {
|
|
72
|
+
return false
|
|
73
|
+
}
|
|
74
|
+
val since = msSinceAutoReconnectLink ?: return false
|
|
75
|
+
return since in 0..BOND_REMOVED_AFTER_RECONNECT_MS
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Tracks rejections across automatic reconnects. Owner loss needs [REJECTIONS_TO_STAND_DOWN]
|
|
80
|
+
* attempts in a row that span at least [MIN_REJECTION_SPAN_MS]. Any successful ready session
|
|
81
|
+
* calls [reset].
|
|
82
|
+
*/
|
|
83
|
+
class OwnerRejectionStreak {
|
|
84
|
+
private var count = 0
|
|
85
|
+
private var firstAtMs = 0L
|
|
86
|
+
private var lastAtMs = 0L
|
|
87
|
+
|
|
88
|
+
/** Records one rejection and returns true once the streak means owner loss. */
|
|
89
|
+
@Synchronized
|
|
90
|
+
fun record(nowMs: Long): Boolean {
|
|
91
|
+
if (count > 0 && nowMs - lastAtMs <= SAME_ATTEMPT_MS) {
|
|
92
|
+
return reached(nowMs)
|
|
93
|
+
}
|
|
94
|
+
if (count > 0 && nowMs - lastAtMs > REJECTION_STREAK_GAP_MS) {
|
|
95
|
+
count = 0
|
|
96
|
+
}
|
|
97
|
+
if (count == 0) firstAtMs = nowMs
|
|
98
|
+
count++
|
|
99
|
+
lastAtMs = nowMs
|
|
100
|
+
return reached(nowMs)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@Synchronized
|
|
104
|
+
fun reset() {
|
|
105
|
+
count = 0
|
|
106
|
+
firstAtMs = 0L
|
|
107
|
+
lastAtMs = 0L
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private fun reached(nowMs: Long): Boolean =
|
|
111
|
+
count >= REJECTIONS_TO_STAND_DOWN && nowMs - firstAtMs >= MIN_REJECTION_SPAN_MS
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* How long automatic reconnect pauses for a window: the window plus the close-deferral
|
|
116
|
+
* margin. No floor beyond a missing value and no upper clamp; the glasses own the window.
|
|
117
|
+
*/
|
|
118
|
+
fun yieldMs(windowMs: Long): Long =
|
|
119
|
+
(if (windowMs > 0L) windowMs else DEFAULT_PAIRING_WINDOW_MS) + PAIRING_RESUME_MARGIN_MS
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Remaining pause after a process restart. A missing, expired, or implausible deadline (more
|
|
123
|
+
* than the pause that was persisted with it) returns 0 so the phone reconnects. Mirrors iOS
|
|
124
|
+
* `MentraLiveConnectionAttemptPolicy.persistedYieldRemainingMs`.
|
|
125
|
+
*/
|
|
126
|
+
fun persistedYieldRemainingMs(untilWallMs: Long, nowWallMs: Long, pauseMs: Long): Long {
|
|
127
|
+
if (untilWallMs <= 0L || pauseMs <= 0L) return 0L
|
|
128
|
+
val remaining = untilWallMs - nowWallMs
|
|
129
|
+
if (remaining <= 0L || remaining > pauseMs) return 0L
|
|
130
|
+
return remaining
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Automatic reconnect must not take the single BLE slot while the saved glasses advertise
|
|
135
|
+
* pairing mode. An explicit pairing attempt (the new phone, or Pair Again) still connects.
|
|
136
|
+
* Mirrors iOS `MentraLiveConnectionAttemptPolicy.shouldStandDownInsteadOfReconnect`.
|
|
137
|
+
*/
|
|
138
|
+
fun shouldStandDownInsteadOfReconnect(
|
|
139
|
+
pairingMode: Boolean,
|
|
140
|
+
savedGlasses: Boolean,
|
|
141
|
+
explicitPairing: Boolean,
|
|
142
|
+
): Boolean = pairingMode && savedGlasses && !explicitPairing
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* A discovery scan lists other secure glasses that are not in pairing mode as non-pairable
|
|
146
|
+
* and does not connect to them. The saved glasses are the exception: the pause watch scan
|
|
147
|
+
* has to read their trailer, so a cleared pairing flag ends the pause and reconnects.
|
|
148
|
+
* Mirrors iOS, which skips this return when the advertisement is the saved device.
|
|
149
|
+
*/
|
|
150
|
+
fun shouldExposeAsNonPairable(
|
|
151
|
+
savedGlasses: Boolean,
|
|
152
|
+
reconnectScan: Boolean,
|
|
153
|
+
secureTrailer: Boolean,
|
|
154
|
+
pairingDiscoverable: Boolean,
|
|
155
|
+
): Boolean = !savedGlasses && !reconnectScan && secureTrailer && !pairingDiscoverable
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* `secure_pairing_capable` only means the firmware can do secure pairing. Ownership is the
|
|
159
|
+
* separate `owner_committed` bit the glasses set after the BLE bond. Mirrors iOS
|
|
160
|
+
* `MentraLiveConnectionAttemptPolicy.pairingInfoConfirmsOwner`.
|
|
161
|
+
*/
|
|
162
|
+
fun pairingInfoConfirmsOwner(ownerCommitted: Boolean): Boolean = ownerCommitted
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Secure glasses commit an owner only on a BLE bond, and announce it with `pairing_info`
|
|
166
|
+
* `owner_committed`. A stale BOND_BONDED (or a Classic-only bond on the same public address)
|
|
167
|
+
* does not prove the glasses committed this phone, so readiness inside the open window needs
|
|
168
|
+
* both the bond and the owner commit. Callers apply this only to explicit pairing attempts;
|
|
169
|
+
* non-secure (legacy) targets are never gated.
|
|
170
|
+
*/
|
|
171
|
+
fun shouldDeferReadyForBond(
|
|
172
|
+
securePairingTarget: Boolean,
|
|
173
|
+
bondState: Int?,
|
|
174
|
+
ownerCommitted: Boolean,
|
|
175
|
+
): Boolean =
|
|
176
|
+
securePairingTarget &&
|
|
177
|
+
(bondState != BluetoothDevice.BOND_BONDED ||
|
|
178
|
+
!pairingInfoConfirmsOwner(ownerCommitted))
|
|
179
|
+
|
|
180
|
+
/** How long an explicit secure pair may wait for a bond result before the attempt fails. */
|
|
181
|
+
const val SECURE_BOND_WAIT_MS = 40_000L
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Readiness parked by [shouldDeferReadyForBond] always has a deadline: an explicit secure
|
|
185
|
+
* attempt arms the bond wait, so a missing bond broadcast or owner commit fails the pair
|
|
186
|
+
* instead of leaving it loading. Mirrors iOS `shouldRunSecurePairingWait`.
|
|
187
|
+
*/
|
|
188
|
+
fun shouldRunSecureBondWait(explicitPairing: Boolean, securePairingTarget: Boolean): Boolean =
|
|
189
|
+
explicitPairing && securePairingTarget
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* An explicit secure pair must not call `createBond` before `pairing_info` arrives: the
|
|
193
|
+
* glasses ask for security themselves once they know the link, and an early SMP races it.
|
|
194
|
+
*/
|
|
195
|
+
fun shouldDeferCtkdBond(
|
|
196
|
+
explicitPairing: Boolean,
|
|
197
|
+
secureTarget: Boolean,
|
|
198
|
+
bondState: Int,
|
|
199
|
+
pairingInfoSeen: Boolean,
|
|
200
|
+
): Boolean =
|
|
201
|
+
explicitPairing &&
|
|
202
|
+
secureTarget &&
|
|
203
|
+
bondState != BluetoothDevice.BOND_BONDED &&
|
|
204
|
+
!pairingInfoSeen
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* `pair_select` tells the glasses this link is the user's explicit choice (pairing spec V4).
|
|
208
|
+
* It goes out once per GATT session, only on an explicit attempt to secure glasses, never on
|
|
209
|
+
* an automatic reconnect. Legacy firmware has no use for it.
|
|
210
|
+
*/
|
|
211
|
+
fun shouldSendPairSelect(explicitPairing: Boolean, securePairingTarget: Boolean): Boolean =
|
|
212
|
+
explicitPairing && securePairingTarget
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Classic audio can come up before the BLE bond or the owner commit. During an explicit
|
|
216
|
+
* secure pair that must not save this phone. A reconnect of an already-paired phone can still
|
|
217
|
+
* mark ready.
|
|
218
|
+
*/
|
|
219
|
+
fun shouldMarkReadyFromAudio(
|
|
220
|
+
explicitPairing: Boolean,
|
|
221
|
+
securePairingTarget: Boolean,
|
|
222
|
+
bondState: Int?,
|
|
223
|
+
ownerCommitted: Boolean,
|
|
224
|
+
): Boolean =
|
|
225
|
+
!explicitPairing ||
|
|
226
|
+
!shouldDeferReadyForBond(securePairingTarget, bondState, ownerCommitted)
|
|
227
|
+
}
|
package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingAdvertisement.kt
CHANGED
|
@@ -3,6 +3,15 @@ package com.mentra.bluetoothsdk.sgcs
|
|
|
3
3
|
internal data class MentraLivePairingAdvertisement(
|
|
4
4
|
val pairingMode: Boolean,
|
|
5
5
|
val pairingCode: String,
|
|
6
|
+
/**
|
|
7
|
+
* The glasses' `window_id`: moves on each pairing-mode entry. Null when the byte is absent.
|
|
8
|
+
*/
|
|
9
|
+
val pairingGeneration: Int? = null,
|
|
10
|
+
/**
|
|
11
|
+
* Trailer flag `OWNER_CHANGED`: the window [pairingGeneration] committed a different phone.
|
|
12
|
+
* Unauthenticated, so it only triggers one verification reconnect.
|
|
13
|
+
*/
|
|
14
|
+
val ownerChanged: Boolean = false,
|
|
6
15
|
)
|
|
7
16
|
|
|
8
17
|
internal object MentraLivePairingAdvertisementParser {
|
|
@@ -14,9 +23,11 @@ internal object MentraLivePairingAdvertisementParser {
|
|
|
14
23
|
private const val CODE_HIGH_OFFSET = CODE_LOW_OFFSET + 1
|
|
15
24
|
private const val MAGIC_FIRST_OFFSET = CODE_HIGH_OFFSET + 1
|
|
16
25
|
private const val MAGIC_SECOND_OFFSET = MAGIC_FIRST_OFFSET + 1
|
|
26
|
+
private const val PAIRING_GENERATION_OFFSET = MAGIC_SECOND_OFFSET + 1
|
|
17
27
|
private const val PROTOCOL_VERSION_MIN = 2
|
|
18
28
|
private const val PROTOCOL_VERSION_MAX = 15
|
|
19
29
|
private const val SECURE_PAIRING_CAPABILITY = 0x01
|
|
30
|
+
private const val OWNER_CHANGED_FLAG = 0x04
|
|
20
31
|
private const val MAGIC_FIRST = 0x4D // M
|
|
21
32
|
private const val MAGIC_SECOND = 0x50 // P
|
|
22
33
|
|
|
@@ -46,9 +57,17 @@ internal object MentraLivePairingAdvertisementParser {
|
|
|
46
57
|
|
|
47
58
|
val codeLow = manufacturerData[CODE_LOW_OFFSET].toInt() and 0xFF
|
|
48
59
|
val codeHigh = manufacturerData[CODE_HIGH_OFFSET].toInt() and 0xFF
|
|
60
|
+
val pairingGeneration =
|
|
61
|
+
if (manufacturerData.size > PAIRING_GENERATION_OFFSET) {
|
|
62
|
+
manufacturerData[PAIRING_GENERATION_OFFSET].toInt() and 0xFF
|
|
63
|
+
} else {
|
|
64
|
+
null
|
|
65
|
+
}
|
|
49
66
|
return MentraLivePairingAdvertisement(
|
|
50
67
|
pairingMode = manufacturerData[PAIRING_FLAG_OFFSET] == PAIRING_DISCOVERABLE,
|
|
51
68
|
pairingCode = String.format("%02X%02X", codeHigh, codeLow),
|
|
69
|
+
pairingGeneration = pairingGeneration,
|
|
70
|
+
ownerChanged = (capability and OWNER_CHANGED_FLAG) != 0,
|
|
52
71
|
)
|
|
53
72
|
}
|
|
54
73
|
}
|
package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingReadinessPolicy.kt
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.sgcs
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pure rules for when an explicit Mentra Live pair reports ready, and for the saved secure
|
|
5
|
+
* firmware memory. [MentraLive] feeds them the selected scan row, this GATT session's
|
|
6
|
+
* `pairing_info` and the bond state. Readiness never waits without a deadline: the owner-commit
|
|
7
|
+
* wait has [MentraLiveOwnerLossPolicy.SECURE_BOND_WAIT_MS], the unknown-capability wait has
|
|
8
|
+
* [UNKNOWN_CAPABILITY_PAIRING_INFO_WAIT_MS].
|
|
9
|
+
*/
|
|
10
|
+
internal object MentraLivePairingReadinessPolicy {
|
|
11
|
+
/**
|
|
12
|
+
* How long an explicit pair to glasses of unknown secure capability waits after
|
|
13
|
+
* `glasses_ready` for `pairing_info` before treating them as legacy firmware.
|
|
14
|
+
*/
|
|
15
|
+
const val UNKNOWN_CAPABILITY_PAIRING_INFO_WAIT_MS = 5_000L
|
|
16
|
+
|
|
17
|
+
/** A ready session without `pairing_info` within this time means the glasses are not secure firmware. */
|
|
18
|
+
const val SECURE_FIRMWARE_REFRESH_MS = 15_000L
|
|
19
|
+
|
|
20
|
+
enum class Readiness {
|
|
21
|
+
/** Report the glasses ready now. */
|
|
22
|
+
READY,
|
|
23
|
+
/** The selected glasses' capability is unknown and no `pairing_info` has arrived yet. */
|
|
24
|
+
AWAIT_PAIRING_INFO,
|
|
25
|
+
/** Secure target: wait for the BLE bond and `pairing_info.owner_committed`. */
|
|
26
|
+
AWAIT_OWNER_COMMIT,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Whether an explicit pair targets secure-pairing firmware. The selected scan row's
|
|
31
|
+
* capability decides, never an address or name seen earlier; when the row did not know it,
|
|
32
|
+
* this session's `pairing_info` does.
|
|
33
|
+
*
|
|
34
|
+
* @param selectedCapability `pending_device_secure_pairing_capable`, null when unknown
|
|
35
|
+
* @param sessionPairingInfoSecure `secure_pairing_capable` from this session's `pairing_info`,
|
|
36
|
+
* null before one arrives
|
|
37
|
+
*/
|
|
38
|
+
fun isSecurePairingTarget(selectedCapability: Boolean?, sessionPairingInfoSecure: Boolean?): Boolean =
|
|
39
|
+
selectedCapability ?: (sessionPairingInfoSecure == true)
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Readiness after `glasses_ready`. Automatic reconnects are never gated. An explicit pair to
|
|
43
|
+
* glasses of unknown capability waits for `pairing_info` until [unknownCapabilityWaitExpired];
|
|
44
|
+
* a secure one then follows the owner-commit rule, a legacy one (or none) is ready.
|
|
45
|
+
*/
|
|
46
|
+
fun readiness(
|
|
47
|
+
explicitPairing: Boolean,
|
|
48
|
+
selectedCapability: Boolean?,
|
|
49
|
+
sessionPairingInfoSecure: Boolean?,
|
|
50
|
+
unknownCapabilityWaitExpired: Boolean,
|
|
51
|
+
bondState: Int?,
|
|
52
|
+
ownerCommitted: Boolean,
|
|
53
|
+
): Readiness {
|
|
54
|
+
if (!explicitPairing) return Readiness.READY
|
|
55
|
+
if (selectedCapability == null &&
|
|
56
|
+
sessionPairingInfoSecure == null &&
|
|
57
|
+
!unknownCapabilityWaitExpired
|
|
58
|
+
) {
|
|
59
|
+
return Readiness.AWAIT_PAIRING_INFO
|
|
60
|
+
}
|
|
61
|
+
val secure = isSecurePairingTarget(selectedCapability, sessionPairingInfoSecure)
|
|
62
|
+
return if (MentraLiveOwnerLossPolicy.shouldDeferReadyForBond(secure, bondState, ownerCommitted)) {
|
|
63
|
+
Readiness.AWAIT_OWNER_COMMIT
|
|
64
|
+
} else {
|
|
65
|
+
Readiness.READY
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* `mentra_live_secure_firmware` once a ready session's [SECURE_FIRMWARE_REFRESH_MS] has
|
|
71
|
+
* passed: false when no `pairing_info` arrived, so glasses rolled back to old firmware stop
|
|
72
|
+
* counting as secure. Null leaves the value a `pairing_info` already wrote.
|
|
73
|
+
*/
|
|
74
|
+
fun secureFirmwareAfterReadySession(pairingInfoSeen: Boolean): Boolean? =
|
|
75
|
+
if (pairingInfoSeen) null else false
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Addresses advertising the secure-pairing trailer in the current scan. It starts empty with each
|
|
80
|
+
* scan, so glasses rolled back to old firmware stop counting as secure on the next scan. It is a
|
|
81
|
+
* hint for automatic sessions only; an explicit pair uses the selected row's capability
|
|
82
|
+
* ([MentraLivePairingReadinessPolicy.isSecurePairingTarget]).
|
|
83
|
+
*/
|
|
84
|
+
internal class MentraLiveSecureTrailerScanMemory {
|
|
85
|
+
private val addresses: MutableSet<String> = java.util.Collections.synchronizedSet(mutableSetOf())
|
|
86
|
+
|
|
87
|
+
fun onScanStart() = addresses.clear()
|
|
88
|
+
|
|
89
|
+
fun noteSecureTrailer(address: String) {
|
|
90
|
+
addresses.add(address)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
fun contains(address: String?): Boolean = address != null && addresses.contains(address)
|
|
94
|
+
}
|