Skip to content

Commit b66485b

Browse files
v1.5.4+307: scan-race de-dup, BLUETOOTH_ADVERTISE, Field Link integration tests
1 parent 1d0eadd commit b66485b

3 files changed

Lines changed: 562 additions & 5 deletions

File tree

lib/ui/screens/field_link/widgets/session_join_card.dart

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,29 @@ class _SessionJoinCardState extends ConsumerState<SessionJoinCard> {
5353
return false;
5454
}
5555
} else {
56+
// Android: SCAN + CONNECT + ADVERTISE. The joiner also advertises
57+
// now (v1.5.4) so a third teammate can find them mid-session, and
58+
// so the creator's slot isn't the single point of failure if its
59+
// BLE advertising is rate-limited. Without ADVERTISE granted,
60+
// BleAdvertiserChannel.kt silently fails when we try to start
61+
// the GATT server from joinSession.
5662
final scanStatus = await Permission.bluetoothScan.status;
5763
final connectStatus = await Permission.bluetoothConnect.status;
58-
if (!scanStatus.isGranted || !connectStatus.isGranted) {
64+
final advertiseStatus = await Permission.bluetoothAdvertise.status;
65+
if (!scanStatus.isGranted ||
66+
!connectStatus.isGranted ||
67+
!advertiseStatus.isGranted) {
5968
final results = await [
6069
Permission.bluetoothScan,
6170
Permission.bluetoothConnect,
71+
Permission.bluetoothAdvertise,
6272
].request();
63-
final allGranted = results.values.every((s) => s.isGranted);
64-
if (!allGranted && mounted) {
73+
// SCAN and CONNECT are hard requirements. ADVERTISE is soft —
74+
// without it the joiner can still operate as a central-only
75+
// client, just won't be discoverable to a third teammate.
76+
final hardGranted = (results[Permission.bluetoothScan]?.isGranted ?? false) &&
77+
(results[Permission.bluetoothConnect]?.isGranted ?? false);
78+
if (!hardGranted && mounted) {
6579
_showScanError('Bluetooth permission required. Enable in Settings.');
6680
return false;
6781
}
@@ -112,8 +126,29 @@ class _SessionJoinCardState extends ConsumerState<SessionJoinCard> {
112126
deviceSub = service.discoveredSessionsStream.listen((device) {
113127
if (!mounted) return;
114128
setState(() {
115-
if (!_discoveredDevices.any((d) => d.id == device.id)) {
129+
// Some BLE stacks deliver the service UUID in the first scan
130+
// callback and the service data (which carries our sessionId)
131+
// in a later one. If we already have this device WITHOUT a
132+
// sessionId and the new result DOES carry one, replace the
133+
// entry in place so a user-tap joins the correct session
134+
// instead of erroring out with "this host did not broadcast
135+
// its session id".
136+
final existingIndex =
137+
_discoveredDevices.indexWhere((d) => d.id == device.id);
138+
if (existingIndex == -1) {
116139
_discoveredDevices.add(device);
140+
} else {
141+
final existing = _discoveredDevices[existingIndex];
142+
final needsUpgrade = (existing.sessionId == null ||
143+
existing.sessionId!.isEmpty) &&
144+
device.sessionId != null &&
145+
device.sessionId!.isNotEmpty;
146+
// Also upgrade if the RSSI changed meaningfully so the list
147+
// reflects current signal strength.
148+
final rssiChanged = (existing.rssi ?? 0) != (device.rssi ?? 0);
149+
if (needsUpgrade || rssiChanged) {
150+
_discoveredDevices[existingIndex] = device;
151+
}
117152
}
118153
});
119154
});

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: red_grid_link
22
description: Offline-first MGRS-native proximity coordination platform
33
publish_to: 'none'
4-
version: 1.5.4+306
4+
version: 1.5.4+307
55

66
environment:
77
sdk: '>=3.2.0 <4.0.0'

0 commit comments

Comments
 (0)