diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt index 3ac87764e..a62ad09bd 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt @@ -302,7 +302,23 @@ class BluetoothConnectionManager( * Public: connect/disconnect helpers for debug UI */ fun connectToAddress(address: String): Boolean = clientManager.connectToAddress(address) - fun disconnectAddress(address: String) { connectionTracker.disconnectDevice(address) } + fun disconnectAddress(address: String) { + connectionScope.launch { + try { + val dc = connectionTracker.getDeviceConnection(address) + if (dc != null) { + if (dc.gatt != null) { + try { dc.gatt.disconnect() } catch (_: Exception) { } + } else { + // Try canceling server-side connection if present + try { serverManager.getGattServer()?.cancelConnection(dc.device) } catch (_: Exception) { } + } + } + } catch (_: Exception) { } + // Cleanup tracking regardless + connectionTracker.cleanupDeviceConnection(address) + } + } // Optionally disconnect all connections (server and client) diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionTracker.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionTracker.kt index 745a1a397..a9d231182 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionTracker.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionTracker.kt @@ -124,6 +124,23 @@ class BluetoothConnectionTracker( fun getSubscribedDevices(): List { return subscribedDevices.toList() } + + /** + * Check whether a given peer already has an active direct connection. + */ + fun isPeerDirectlyConnected(peerID: String): Boolean { + val address = getConnectedAddressForPeer(peerID) + return address != null + } + + /** + * Return the connected device address for a peer, if currently connected. + */ + fun getConnectedAddressForPeer(peerID: String): String? { + // Find any address mapped to this peer that is still connected + val address = addressPeerMap.entries.firstOrNull { it.value == peerID }?.key + return address?.takeIf { connectedDevices.containsKey(it) } + } /** * Get current RSSI for a device address @@ -198,7 +215,8 @@ class BluetoothConnectionTracker( } // Update connection attempt atomically - val attempts = (currentAttempt?.attempts ?: 0) + 1 + // If the previous attempt window expired, reset backoff to 1; otherwise increment + val attempts = if (currentAttempt?.isExpired() == true) 1 else (currentAttempt?.attempts ?: 0) + 1 pendingConnections[deviceAddress] = ConnectionAttempt(attempts) Log.d(TAG, "Tracker: Added pending connection for $deviceAddress (attempts: $attempts)") return true @@ -283,7 +301,6 @@ class BluetoothConnectionTracker( subscribedDevices.removeAll { it.address == deviceAddress } addressPeerMap.remove(deviceAddress) } - pendingConnections.remove(deviceAddress) firstAnnounceSeen.remove(deviceAddress) Log.d(TAG, "Cleaned up device connection for $deviceAddress") } diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt index ee5f0f167..111022816 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt @@ -408,18 +408,28 @@ class BluetoothMeshService(private val context: Context) { val deviceAddress = routed.relayAddress val pid = routed.peerID if (deviceAddress != null && pid != null) { - // First ANNOUNCE over a device connection defines a direct neighbor. + // Only process on first ANNOUNCE seen for this device connection if (!connectionManager.hasSeenFirstAnnounce(deviceAddress)) { - // Bind or rebind this device address to the announcing peer - connectionManager.addressPeerMap[deviceAddress] = pid - connectionManager.noteAnnounceReceived(deviceAddress) - Log.d(TAG, "Mapped device $deviceAddress to peer $pid on FIRST-ANNOUNCE for this connection") + // If we're already directly connected to this peer via another address, drop the new one. + val existingAddress = connectionManager.addressPeerMap + .entries.firstOrNull { it.value == pid }?.key - // Mark as directly connected (upgrades from routed if needed) - try { peerManager.setDirectConnection(pid, true) } catch (_: Exception) { } + val existingConnected = existingAddress != null && + connectionManager.isClientConnection(existingAddress!!) != null - // Initial sync for this newly direct peer - try { gossipSyncManager.scheduleInitialSyncToPeer(pid, 1_000) } catch (_: Exception) { } + if (existingAddress != null && existingAddress != deviceAddress && existingConnected) { + Log.i(TAG, "Peer $pid already connected via $existingAddress; dropping new connection $deviceAddress") + // Disconnect the newer duplicate connection + connectionManager.disconnectAddress(deviceAddress) + } else { + // Bind this device address to peer and mark direct + connectionManager.addressPeerMap[deviceAddress] = pid + connectionManager.noteAnnounceReceived(deviceAddress) + Log.d(TAG, "Mapped device $deviceAddress to peer $pid on FIRST-ANNOUNCE for this connection") + + try { peerManager.setDirectConnection(pid, true) } catch (_: Exception) { } + try { gossipSyncManager.scheduleInitialSyncToPeer(pid, 1_000) } catch (_: Exception) { } + } } } // Track for sync