@@ -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 });
0 commit comments