@mentra/bluetooth-sdk 3.4.0-dev.996 → 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,368 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.sgcs
|
|
2
|
+
|
|
3
|
+
import android.bluetooth.BluetoothDevice
|
|
4
|
+
import org.junit.Assert.assertEquals
|
|
5
|
+
import org.junit.Assert.assertFalse
|
|
6
|
+
import org.junit.Assert.assertTrue
|
|
7
|
+
import org.junit.Test
|
|
8
|
+
|
|
9
|
+
class MentraLiveOwnerLossPolicyTest {
|
|
10
|
+
@Test
|
|
11
|
+
fun connectTimeRejectionIsAHintOnlyOutsideExplicitPairingOnSecureFirmware() {
|
|
12
|
+
assertTrue(gattRejection(5))
|
|
13
|
+
assertTrue(gattRejection(15))
|
|
14
|
+
assertFalse(gattRejection(5, manualPairing = true))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@Test
|
|
18
|
+
fun legacyFirmwareRejectionsAreNeverOwnerLoss() {
|
|
19
|
+
assertFalse(gattRejection(5, secureFirmware = false))
|
|
20
|
+
assertFalse(gattRejection(15, secureFirmware = false))
|
|
21
|
+
assertFalse(bondRemoved(msSinceAutoReconnectLink = 600L, secureFirmware = false))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@Test
|
|
25
|
+
fun rfDropsAndRemoteTerminationAreNotOwnerLoss() {
|
|
26
|
+
for (status in listOf(8, 19, 22, 62, 133)) {
|
|
27
|
+
assertFalse("status $status", gattRejection(status))
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@Test
|
|
32
|
+
fun bondDroppedRightAfterAutomaticReconnectIsARejection() {
|
|
33
|
+
assertTrue(bondRemoved(msSinceAutoReconnectLink = 600L))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@Test
|
|
37
|
+
fun bondRemovalOutsideAReconnectIsNotARejection() {
|
|
38
|
+
assertFalse(bondRemoved(msSinceAutoReconnectLink = null))
|
|
39
|
+
assertFalse(bondRemoved(msSinceAutoReconnectLink = 60_000L))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@Test
|
|
43
|
+
fun appUnpairAndExplicitPairingNeverReportOwnerLoss() {
|
|
44
|
+
assertFalse(bondRemoved(msSinceAutoReconnectLink = 600L, killed = true))
|
|
45
|
+
assertFalse(bondRemoved(msSinceAutoReconnectLink = 600L, unpairInProgress = true))
|
|
46
|
+
assertFalse(bondRemoved(msSinceAutoReconnectLink = 600L, manualPairing = true))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@Test
|
|
50
|
+
fun aSingleRejectionDoesNotStandDown() {
|
|
51
|
+
val streak = MentraLiveOwnerLossPolicy.OwnerRejectionStreak()
|
|
52
|
+
assertFalse(streak.record(0L))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@Test
|
|
56
|
+
fun threeQuickRejectionsDoNotStandDownUntilTheStreakSpansAboutAMinute() {
|
|
57
|
+
val streak = MentraLiveOwnerLossPolicy.OwnerRejectionStreak()
|
|
58
|
+
assertFalse(streak.record(0L))
|
|
59
|
+
assertFalse(streak.record(3_000L))
|
|
60
|
+
// Third attempt, but a glasses reboot also rejects for a few seconds.
|
|
61
|
+
assertFalse(streak.record(8_000L))
|
|
62
|
+
assertFalse(streak.record(24_000L))
|
|
63
|
+
assertTrue(streak.record(MentraLiveOwnerLossPolicy.MIN_REJECTION_SPAN_MS))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@Test
|
|
67
|
+
fun aSuccessfulSessionEndsTheStreak() {
|
|
68
|
+
val streak = MentraLiveOwnerLossPolicy.OwnerRejectionStreak()
|
|
69
|
+
streak.record(0L)
|
|
70
|
+
streak.record(10_000L)
|
|
71
|
+
streak.reset()
|
|
72
|
+
assertFalse(streak.record(50_000L))
|
|
73
|
+
assertFalse(streak.record(60_000L))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@Test
|
|
77
|
+
fun rejectionsFarApartAreNotOneStreak() {
|
|
78
|
+
val streak = MentraLiveOwnerLossPolicy.OwnerRejectionStreak()
|
|
79
|
+
streak.record(0L)
|
|
80
|
+
streak.record(10_000L)
|
|
81
|
+
streak.record(20_000L)
|
|
82
|
+
// A long quiet gap means the earlier rejections were something else.
|
|
83
|
+
val later = 20_000L + MentraLiveOwnerLossPolicy.REJECTION_STREAK_GAP_MS + 1L
|
|
84
|
+
assertFalse(streak.record(later))
|
|
85
|
+
assertFalse(streak.record(later + 10_000L))
|
|
86
|
+
assertFalse(streak.record(later + 20_000L))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@Test
|
|
90
|
+
fun oneAttemptReportingTwiceCountsOnce() {
|
|
91
|
+
val streak = MentraLiveOwnerLossPolicy.OwnerRejectionStreak()
|
|
92
|
+
val span = MentraLiveOwnerLossPolicy.MIN_REJECTION_SPAN_MS
|
|
93
|
+
// Bond removed, then the GATT disconnect with status 5, for each of two attempts.
|
|
94
|
+
// Counted per signal this would be four rejections over a long enough span.
|
|
95
|
+
assertFalse(streak.record(0L))
|
|
96
|
+
assertFalse(streak.record(400L))
|
|
97
|
+
assertFalse(streak.record(span))
|
|
98
|
+
assertFalse(streak.record(span + 400L))
|
|
99
|
+
// A genuine third attempt completes the streak.
|
|
100
|
+
assertTrue(streak.record(span + 10_000L))
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@Test
|
|
104
|
+
fun aFailedNewBondIsNotABondRemoval() {
|
|
105
|
+
assertFalse(
|
|
106
|
+
MentraLiveOwnerLossPolicy.isOwnerRejectionOnBondRemoved(
|
|
107
|
+
previousBondState = BluetoothDevice.BOND_BONDING,
|
|
108
|
+
bondState = BluetoothDevice.BOND_NONE,
|
|
109
|
+
killed = false,
|
|
110
|
+
unpairInProgress = false,
|
|
111
|
+
manualPairing = false,
|
|
112
|
+
msSinceAutoReconnectLink = 600L,
|
|
113
|
+
secureFirmware = true,
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@Test
|
|
119
|
+
fun pauseIsTheWindowPlusTheCloseMarginWithNo300sClamp() {
|
|
120
|
+
assertEquals(150_000L, MentraLiveOwnerLossPolicy.yieldMs(120_000L))
|
|
121
|
+
// The glasses own the window: a longer one is paused for in full, not clamped to 300 s.
|
|
122
|
+
assertEquals(930_000L, MentraLiveOwnerLossPolicy.yieldMs(900_000L))
|
|
123
|
+
// A short window is not stretched to a floor either.
|
|
124
|
+
assertEquals(40_000L, MentraLiveOwnerLossPolicy.yieldMs(10_000L))
|
|
125
|
+
// A missing window falls back to the 120 s default.
|
|
126
|
+
assertEquals(150_000L, MentraLiveOwnerLossPolicy.yieldMs(0L))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
@Test
|
|
130
|
+
fun aRestartedAppKeepsOnlyALiveDeadlineWithinThePersistedPause() {
|
|
131
|
+
val now = 1_000_000L
|
|
132
|
+
val pause = MentraLiveOwnerLossPolicy.yieldMs(120_000L)
|
|
133
|
+
assertEquals(0L, MentraLiveOwnerLossPolicy.persistedYieldRemainingMs(0L, now, pause))
|
|
134
|
+
assertEquals(50_000L, MentraLiveOwnerLossPolicy.persistedYieldRemainingMs(now + 50_000L, now, pause))
|
|
135
|
+
assertEquals(pause, MentraLiveOwnerLossPolicy.persistedYieldRemainingMs(now + pause, now, pause))
|
|
136
|
+
assertEquals(0L, MentraLiveOwnerLossPolicy.persistedYieldRemainingMs(now - 1L, now, pause))
|
|
137
|
+
// Further out than the pause that was recorded with it: a clock change, not a window.
|
|
138
|
+
assertEquals(0L, MentraLiveOwnerLossPolicy.persistedYieldRemainingMs(now + pause + 1L, now, pause))
|
|
139
|
+
assertEquals(0L, MentraLiveOwnerLossPolicy.persistedYieldRemainingMs(now + 1_000L, now, 0L))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@Test
|
|
143
|
+
fun theWindowRefusalStatus0x14IsNeverARejection() {
|
|
144
|
+
assertFalse(gattRejection(0x14))
|
|
145
|
+
assertFalse(MentraLiveOwnerLossPolicy.isOwnerRejectionStatus(0x14))
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@Test
|
|
149
|
+
fun pairSelectGoesOnlyWithAnExplicitAttemptToSecureGlasses() {
|
|
150
|
+
assertTrue(MentraLiveOwnerLossPolicy.shouldSendPairSelect(explicitPairing = true, securePairingTarget = true))
|
|
151
|
+
assertFalse(MentraLiveOwnerLossPolicy.shouldSendPairSelect(explicitPairing = false, securePairingTarget = true))
|
|
152
|
+
assertFalse(MentraLiveOwnerLossPolicy.shouldSendPairSelect(explicitPairing = true, securePairingTarget = false))
|
|
153
|
+
assertFalse(MentraLiveOwnerLossPolicy.shouldSendPairSelect(explicitPairing = false, securePairingTarget = false))
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@Test
|
|
157
|
+
fun parkedSecureReadinessAlwaysHasABondWaitDeadline() {
|
|
158
|
+
// Nothing (no awaiting_confirm any more) suspends the wait on an explicit secure pair.
|
|
159
|
+
assertTrue(MentraLiveOwnerLossPolicy.shouldRunSecureBondWait(explicitPairing = true, securePairingTarget = true))
|
|
160
|
+
assertFalse(MentraLiveOwnerLossPolicy.shouldRunSecureBondWait(explicitPairing = false, securePairingTarget = true))
|
|
161
|
+
assertFalse(MentraLiveOwnerLossPolicy.shouldRunSecureBondWait(explicitPairing = true, securePairingTarget = false))
|
|
162
|
+
assertEquals(40_000L, MentraLiveOwnerLossPolicy.SECURE_BOND_WAIT_MS)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
@Test
|
|
166
|
+
fun explicitSecurePairDefersCtkdUntilPairingInfoArrives() {
|
|
167
|
+
assertTrue(
|
|
168
|
+
MentraLiveOwnerLossPolicy.shouldDeferCtkdBond(
|
|
169
|
+
explicitPairing = true,
|
|
170
|
+
secureTarget = true,
|
|
171
|
+
bondState = BluetoothDevice.BOND_NONE,
|
|
172
|
+
pairingInfoSeen = false,
|
|
173
|
+
),
|
|
174
|
+
)
|
|
175
|
+
assertFalse(
|
|
176
|
+
MentraLiveOwnerLossPolicy.shouldDeferCtkdBond(
|
|
177
|
+
explicitPairing = true,
|
|
178
|
+
secureTarget = true,
|
|
179
|
+
bondState = BluetoothDevice.BOND_NONE,
|
|
180
|
+
pairingInfoSeen = true,
|
|
181
|
+
),
|
|
182
|
+
)
|
|
183
|
+
assertFalse(
|
|
184
|
+
MentraLiveOwnerLossPolicy.shouldDeferCtkdBond(
|
|
185
|
+
explicitPairing = false,
|
|
186
|
+
secureTarget = true,
|
|
187
|
+
bondState = BluetoothDevice.BOND_NONE,
|
|
188
|
+
pairingInfoSeen = false,
|
|
189
|
+
),
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
@Test
|
|
194
|
+
fun aClearedPairingFlagOnTheSavedGlassesIsNotHiddenFromThePauseWatchScan() {
|
|
195
|
+
assertFalse(
|
|
196
|
+
MentraLiveOwnerLossPolicy.shouldExposeAsNonPairable(
|
|
197
|
+
savedGlasses = true,
|
|
198
|
+
reconnectScan = false,
|
|
199
|
+
secureTrailer = true,
|
|
200
|
+
pairingDiscoverable = false,
|
|
201
|
+
)
|
|
202
|
+
)
|
|
203
|
+
assertTrue(
|
|
204
|
+
MentraLiveOwnerLossPolicy.shouldExposeAsNonPairable(
|
|
205
|
+
savedGlasses = false,
|
|
206
|
+
reconnectScan = false,
|
|
207
|
+
secureTrailer = true,
|
|
208
|
+
pairingDiscoverable = false,
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
assertFalse(
|
|
212
|
+
MentraLiveOwnerLossPolicy.shouldExposeAsNonPairable(
|
|
213
|
+
savedGlasses = false,
|
|
214
|
+
reconnectScan = false,
|
|
215
|
+
secureTrailer = true,
|
|
216
|
+
pairingDiscoverable = true,
|
|
217
|
+
)
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
@Test
|
|
222
|
+
fun automaticReconnectStandsDownWhileTheSavedGlassesAdvertisePairingMode() {
|
|
223
|
+
assertTrue(
|
|
224
|
+
MentraLiveOwnerLossPolicy.shouldStandDownInsteadOfReconnect(
|
|
225
|
+
pairingMode = true,
|
|
226
|
+
savedGlasses = true,
|
|
227
|
+
explicitPairing = false,
|
|
228
|
+
)
|
|
229
|
+
)
|
|
230
|
+
assertFalse(
|
|
231
|
+
MentraLiveOwnerLossPolicy.shouldStandDownInsteadOfReconnect(
|
|
232
|
+
pairingMode = true,
|
|
233
|
+
savedGlasses = true,
|
|
234
|
+
explicitPairing = true,
|
|
235
|
+
)
|
|
236
|
+
)
|
|
237
|
+
assertFalse(
|
|
238
|
+
MentraLiveOwnerLossPolicy.shouldStandDownInsteadOfReconnect(
|
|
239
|
+
pairingMode = false,
|
|
240
|
+
savedGlasses = true,
|
|
241
|
+
explicitPairing = false,
|
|
242
|
+
)
|
|
243
|
+
)
|
|
244
|
+
assertFalse(
|
|
245
|
+
MentraLiveOwnerLossPolicy.shouldStandDownInsteadOfReconnect(
|
|
246
|
+
pairingMode = true,
|
|
247
|
+
savedGlasses = false,
|
|
248
|
+
explicitPairing = false,
|
|
249
|
+
)
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
@Test
|
|
254
|
+
fun secureTargetIsNotReadyUntilBondedAndOwnerCommitted() {
|
|
255
|
+
assertTrue(defer(bond = BluetoothDevice.BOND_BONDING, committed = true))
|
|
256
|
+
assertTrue(defer(bond = BluetoothDevice.BOND_NONE, committed = true))
|
|
257
|
+
assertTrue(defer(bond = BluetoothDevice.BOND_NONE, committed = false))
|
|
258
|
+
assertFalse(defer(bond = BluetoothDevice.BOND_BONDED, committed = true))
|
|
259
|
+
assertFalse(defer(secure = false, bond = BluetoothDevice.BOND_NONE, committed = false))
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
@Test
|
|
263
|
+
fun staleBondWithoutOwnerCommitStillDefersAnExplicitSecurePair() {
|
|
264
|
+
// A leftover BOND_BONDED (or a Classic-only bond on the same public address) is not
|
|
265
|
+
// proof that the glasses committed this phone as owner.
|
|
266
|
+
assertTrue(defer(bond = BluetoothDevice.BOND_BONDED, committed = false))
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
@Test
|
|
270
|
+
fun bondedAndOwnerCommittedIsNotDeferred() {
|
|
271
|
+
assertFalse(defer(bond = BluetoothDevice.BOND_BONDED, committed = true))
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
@Test
|
|
275
|
+
fun lateOwnerCommitAfterBondAllowsReady() {
|
|
276
|
+
// glasses_ready and the bond arrive first: parked.
|
|
277
|
+
assertTrue(defer(bond = BluetoothDevice.BOND_BONDED, committed = false))
|
|
278
|
+
// The late pairing_info carries owner_committed: ready is allowed now.
|
|
279
|
+
assertTrue(MentraLiveOwnerLossPolicy.pairingInfoConfirmsOwner(true))
|
|
280
|
+
assertFalse(defer(bond = BluetoothDevice.BOND_BONDED, committed = true))
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
@Test
|
|
284
|
+
fun pairingInfoWithoutOwnerCommitDoesNotConfirmOwner() {
|
|
285
|
+
assertFalse(MentraLiveOwnerLossPolicy.pairingInfoConfirmsOwner(false))
|
|
286
|
+
assertTrue(MentraLiveOwnerLossPolicy.pairingInfoConfirmsOwner(true))
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
@Test
|
|
290
|
+
fun legacyTargetIsNeverGated() {
|
|
291
|
+
for (bond in listOf(BluetoothDevice.BOND_NONE, BluetoothDevice.BOND_BONDING, null)) {
|
|
292
|
+
for (committed in listOf(true, false)) {
|
|
293
|
+
assertFalse(defer(secure = false, bond = bond, committed = committed))
|
|
294
|
+
assertTrue(audioReady(secure = false, bond = bond, committed = committed))
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
@Test
|
|
300
|
+
fun classicAudioDoesNotCommitAnUnbondedSecurePair() {
|
|
301
|
+
assertFalse(audioReady(bond = BluetoothDevice.BOND_BONDING, committed = false))
|
|
302
|
+
assertFalse(audioReady(bond = BluetoothDevice.BOND_NONE, committed = false))
|
|
303
|
+
assertTrue(audioReady(bond = BluetoothDevice.BOND_BONDED, committed = true))
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
@Test
|
|
307
|
+
fun classicAudioFirstWithoutOwnerCommitDoesNotMarkReadyDuringExplicitPairing() {
|
|
308
|
+
assertFalse(audioReady(bond = BluetoothDevice.BOND_BONDED, committed = false))
|
|
309
|
+
// Once the commit arrives the same audio-first state may complete.
|
|
310
|
+
assertTrue(audioReady(bond = BluetoothDevice.BOND_BONDED, committed = true))
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
@Test
|
|
314
|
+
fun reconnectOutsideExplicitPairingIsNeverGated() {
|
|
315
|
+
for (bond in listOf(BluetoothDevice.BOND_NONE, BluetoothDevice.BOND_BONDING, null)) {
|
|
316
|
+
for (committed in listOf(true, false)) {
|
|
317
|
+
assertTrue(audioReady(explicit = false, bond = bond, committed = committed))
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private fun defer(
|
|
323
|
+
secure: Boolean = true,
|
|
324
|
+
bond: Int?,
|
|
325
|
+
committed: Boolean,
|
|
326
|
+
): Boolean = MentraLiveOwnerLossPolicy.shouldDeferReadyForBond(secure, bond, committed)
|
|
327
|
+
|
|
328
|
+
private fun audioReady(
|
|
329
|
+
explicit: Boolean = true,
|
|
330
|
+
secure: Boolean = true,
|
|
331
|
+
bond: Int?,
|
|
332
|
+
committed: Boolean,
|
|
333
|
+
): Boolean =
|
|
334
|
+
MentraLiveOwnerLossPolicy.shouldMarkReadyFromAudio(
|
|
335
|
+
explicitPairing = explicit,
|
|
336
|
+
securePairingTarget = secure,
|
|
337
|
+
bondState = bond,
|
|
338
|
+
ownerCommitted = committed,
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
private fun gattRejection(
|
|
342
|
+
status: Int,
|
|
343
|
+
manualPairing: Boolean = false,
|
|
344
|
+
secureFirmware: Boolean = true,
|
|
345
|
+
): Boolean =
|
|
346
|
+
MentraLiveOwnerLossPolicy.isOwnerRejectionOnGattError(
|
|
347
|
+
status,
|
|
348
|
+
manualPairing,
|
|
349
|
+
secureFirmware,
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
private fun bondRemoved(
|
|
353
|
+
msSinceAutoReconnectLink: Long?,
|
|
354
|
+
killed: Boolean = false,
|
|
355
|
+
unpairInProgress: Boolean = false,
|
|
356
|
+
manualPairing: Boolean = false,
|
|
357
|
+
secureFirmware: Boolean = true,
|
|
358
|
+
): Boolean =
|
|
359
|
+
MentraLiveOwnerLossPolicy.isOwnerRejectionOnBondRemoved(
|
|
360
|
+
previousBondState = BluetoothDevice.BOND_BONDED,
|
|
361
|
+
bondState = BluetoothDevice.BOND_NONE,
|
|
362
|
+
killed = killed,
|
|
363
|
+
unpairInProgress = unpairInProgress,
|
|
364
|
+
manualPairing = manualPairing,
|
|
365
|
+
msSinceAutoReconnectLink = msSinceAutoReconnectLink,
|
|
366
|
+
secureFirmware = secureFirmware,
|
|
367
|
+
)
|
|
368
|
+
}
|
|
@@ -81,6 +81,26 @@ class MentraLivePairingAdvertisementParserTest {
|
|
|
81
81
|
assertFalse(result?.pairingMode == true)
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
@Test
|
|
85
|
+
fun readsTheOwnerChangedFlagAndWindowIdFromTheV4Trailer() {
|
|
86
|
+
val payload = securePayload(pairingFlag = 0)
|
|
87
|
+
payload[6] = 4
|
|
88
|
+
payload[7] = 0x05
|
|
89
|
+
payload[12] = 0xC8.toByte()
|
|
90
|
+
val result = MentraLivePairingAdvertisementParser.parse(payload)
|
|
91
|
+
|
|
92
|
+
assertTrue(result?.ownerChanged == true)
|
|
93
|
+
assertEquals(200, result?.pairingGeneration)
|
|
94
|
+
assertFalse(result?.pairingMode == true)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@Test
|
|
98
|
+
fun ownerChangedIsFalseWithoutTheFlag() {
|
|
99
|
+
val result = MentraLivePairingAdvertisementParser.parse(securePayload(pairingFlag = 1))
|
|
100
|
+
|
|
101
|
+
assertFalse(result?.ownerChanged == true)
|
|
102
|
+
}
|
|
103
|
+
|
|
84
104
|
@Test
|
|
85
105
|
fun rejectsLegacyPayloadThatMatchesOldVersionCapabilityHeuristic() {
|
|
86
106
|
val legacyPayload = ByteArray(27)
|
package/android/src/test/java/com/mentra/bluetoothsdk/sgcs/MentraLivePairingReadinessPolicyTest.kt
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
package com.mentra.bluetoothsdk.sgcs
|
|
2
|
+
|
|
3
|
+
import android.bluetooth.BluetoothDevice
|
|
4
|
+
import com.mentra.bluetoothsdk.sgcs.MentraLivePairingReadinessPolicy.Readiness
|
|
5
|
+
import org.junit.Assert.assertEquals
|
|
6
|
+
import org.junit.Assert.assertFalse
|
|
7
|
+
import org.junit.Assert.assertNotEquals
|
|
8
|
+
import org.junit.Assert.assertNull
|
|
9
|
+
import org.junit.Assert.assertTrue
|
|
10
|
+
import org.junit.Test
|
|
11
|
+
|
|
12
|
+
class MentraLivePairingReadinessPolicyTest {
|
|
13
|
+
private val bonded = BluetoothDevice.BOND_BONDED
|
|
14
|
+
private val unbonded = BluetoothDevice.BOND_NONE
|
|
15
|
+
|
|
16
|
+
@Test
|
|
17
|
+
fun `constants match the spec`() {
|
|
18
|
+
assertEquals(5_000L, MentraLivePairingReadinessPolicy.UNKNOWN_CAPABILITY_PAIRING_INFO_WAIT_MS)
|
|
19
|
+
assertEquals(15_000L, MentraLivePairingReadinessPolicy.SECURE_FIRMWARE_REFRESH_MS)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@Test
|
|
23
|
+
fun `the selected row decides and session pairing_info only fills an unknown row`() {
|
|
24
|
+
val policy = MentraLivePairingReadinessPolicy
|
|
25
|
+
assertTrue(policy.isSecurePairingTarget(true, null))
|
|
26
|
+
assertTrue(policy.isSecurePairingTarget(true, false))
|
|
27
|
+
// A row listed as legacy stays legacy (a rolled-back unit is never re-read as secure).
|
|
28
|
+
assertFalse(policy.isSecurePairingTarget(false, true))
|
|
29
|
+
assertFalse(policy.isSecurePairingTarget(false, null))
|
|
30
|
+
assertTrue(policy.isSecurePairingTarget(null, true))
|
|
31
|
+
assertFalse(policy.isSecurePairingTarget(null, false))
|
|
32
|
+
assertFalse(policy.isSecurePairingTarget(null, null))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@Test
|
|
36
|
+
fun `automatic reconnects are never gated`() {
|
|
37
|
+
for (selected in listOf(true, false, null)) for (session in listOf(true, false, null)) {
|
|
38
|
+
assertEquals(
|
|
39
|
+
Readiness.READY,
|
|
40
|
+
readiness(explicit = false, selected = selected, session = session, bond = unbonded),
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@Test
|
|
46
|
+
fun `unknown capability waits for pairing_info, then is legacy and ready without one`() {
|
|
47
|
+
assertEquals(Readiness.AWAIT_PAIRING_INFO, readiness(selected = null, session = null))
|
|
48
|
+
assertEquals(Readiness.READY, readiness(selected = null, session = null, expired = true))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@Test
|
|
52
|
+
fun `unknown capability with a secure pairing_info follows the secure rule`() {
|
|
53
|
+
assertEquals(Readiness.AWAIT_OWNER_COMMIT, readiness(selected = null, session = true, bond = unbonded))
|
|
54
|
+
assertEquals(
|
|
55
|
+
Readiness.AWAIT_OWNER_COMMIT,
|
|
56
|
+
readiness(selected = null, session = true, bond = bonded, committed = false),
|
|
57
|
+
)
|
|
58
|
+
assertEquals(Readiness.READY, readiness(selected = null, session = true, bond = bonded, committed = true))
|
|
59
|
+
// A late secure report after the wait expired still applies the secure rule.
|
|
60
|
+
assertEquals(
|
|
61
|
+
Readiness.AWAIT_OWNER_COMMIT,
|
|
62
|
+
readiness(selected = null, session = true, expired = true, bond = unbonded),
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@Test
|
|
67
|
+
fun `unknown capability with a legacy pairing_info is ready at once`() {
|
|
68
|
+
assertEquals(Readiness.READY, readiness(selected = null, session = false, bond = unbonded))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@Test
|
|
72
|
+
fun `known rows never wait for pairing_info`() {
|
|
73
|
+
assertEquals(Readiness.AWAIT_OWNER_COMMIT, readiness(selected = true, session = null, bond = unbonded))
|
|
74
|
+
assertEquals(Readiness.READY, readiness(selected = true, session = null, bond = bonded, committed = true))
|
|
75
|
+
assertEquals(Readiness.READY, readiness(selected = false, session = null, bond = unbonded))
|
|
76
|
+
assertEquals(Readiness.READY, readiness(selected = false, session = true, bond = unbonded))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@Test
|
|
80
|
+
fun `readiness never waits without a deadline`() {
|
|
81
|
+
for (explicit in listOf(true, false)) for (selected in listOf(true, false, null)) {
|
|
82
|
+
for (session in listOf(true, false, null)) for (expired in listOf(true, false)) {
|
|
83
|
+
for (bond in listOf(bonded, unbonded, BluetoothDevice.BOND_BONDING, null)) {
|
|
84
|
+
for (committed in listOf(true, false)) {
|
|
85
|
+
val result = readiness(explicit, selected, session, expired, bond, committed)
|
|
86
|
+
// The pairing_info wait is bounded: once it expired it never applies.
|
|
87
|
+
if (expired) assertNotEquals(Readiness.AWAIT_PAIRING_INFO, result)
|
|
88
|
+
// The owner-commit wait always runs the secure bond wait deadline.
|
|
89
|
+
if (result == Readiness.AWAIT_OWNER_COMMIT) {
|
|
90
|
+
assertTrue(
|
|
91
|
+
MentraLiveOwnerLossPolicy.shouldRunSecureBondWait(
|
|
92
|
+
explicit,
|
|
93
|
+
MentraLivePairingReadinessPolicy.isSecurePairingTarget(selected, session),
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@Test
|
|
104
|
+
fun `a ready session without pairing_info clears the secure firmware memory`() {
|
|
105
|
+
assertEquals(false, MentraLivePairingReadinessPolicy.secureFirmwareAfterReadySession(pairingInfoSeen = false))
|
|
106
|
+
assertNull(MentraLivePairingReadinessPolicy.secureFirmwareAfterReadySession(pairingInfoSeen = true))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@Test
|
|
110
|
+
fun `secure trailer memory starts empty with each scan`() {
|
|
111
|
+
val memory = MentraLiveSecureTrailerScanMemory()
|
|
112
|
+
memory.noteSecureTrailer("AA:BB:CC:DD:EE:01")
|
|
113
|
+
assertTrue(memory.contains("AA:BB:CC:DD:EE:01"))
|
|
114
|
+
assertFalse(memory.contains("AA:BB:CC:DD:EE:02"))
|
|
115
|
+
assertFalse(memory.contains(null))
|
|
116
|
+
memory.onScanStart()
|
|
117
|
+
assertFalse(memory.contains("AA:BB:CC:DD:EE:01"))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private fun readiness(
|
|
121
|
+
explicit: Boolean = true,
|
|
122
|
+
selected: Boolean?,
|
|
123
|
+
session: Boolean?,
|
|
124
|
+
expired: Boolean = false,
|
|
125
|
+
bond: Int? = unbonded,
|
|
126
|
+
committed: Boolean = false,
|
|
127
|
+
): Readiness =
|
|
128
|
+
MentraLivePairingReadinessPolicy.readiness(
|
|
129
|
+
explicitPairing = explicit,
|
|
130
|
+
selectedCapability = selected,
|
|
131
|
+
sessionPairingInfoSecure = session,
|
|
132
|
+
unknownCapabilityWaitExpired = expired,
|
|
133
|
+
bondState = bond,
|
|
134
|
+
ownerCommitted = committed,
|
|
135
|
+
)
|
|
136
|
+
}
|