From c2f60d6ab2bf684ac7dba9291eb42769d35771c8 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Thu, 15 Jan 2026 13:51:30 +0700 Subject: [PATCH] announce peer ID in servicedata to prevent duplicate connections (#613) --- .../mesh/BluetoothConnectionManager.kt | 2 +- .../mesh/BluetoothConnectionTracker.kt | 11 ++++++- .../mesh/BluetoothGattClientManager.kt | 30 ++++++++++++++----- .../mesh/BluetoothGattServerManager.kt | 21 +++++++++++-- 4 files changed, 52 insertions(+), 12 deletions(-) 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 f6dc194d..cf5975b2 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt @@ -70,7 +70,7 @@ class BluetoothConnectionManager( } private val serverManager = BluetoothGattServerManager( - context, connectionScope, connectionTracker, permissionManager, powerManager, componentDelegate + context, connectionScope, connectionTracker, permissionManager, powerManager, componentDelegate, myPeerID ) private val clientManager = BluetoothGattClientManager( context, connectionScope, connectionTracker, permissionManager, powerManager, componentDelegate 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 85e5f452..7500be21 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionTracker.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionTracker.kt @@ -51,7 +51,8 @@ class BluetoothConnectionTracker( val characteristic: BluetoothGattCharacteristic? = null, val rssi: Int = Int.MIN_VALUE, val isClient: Boolean = false, - val connectedAt: Long = System.currentTimeMillis() + val connectedAt: Long = System.currentTimeMillis(), + val peerID: String? = null ) /** @@ -170,6 +171,14 @@ class BluetoothConnectionTracker( fun isDeviceConnected(deviceAddress: String): Boolean { return connectedDevices.containsKey(deviceAddress) } + + /** + * Check if a peer is already connected (by PeerID) + */ + fun isPeerConnected(peerID: String): Boolean { + // Only consider actual connected devices that have identified themselves + return connectedDevices.values.any { it.peerID == peerID } + } /** * Check if connection attempt is allowed diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothGattClientManager.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothGattClientManager.kt index e5feea0a..7c31e0d3 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothGattClientManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothGattClientManager.kt @@ -320,6 +320,22 @@ class BluetoothGattClientManager( return } + // Try to extract peerID from Service Data (if available) for stable identity + val serviceData = scanRecord?.getServiceData(ParcelUuid(AppConstants.Mesh.Gatt.SERVICE_UUID)) + val peerID = if (serviceData != null && serviceData.size >= 8) { + serviceData.joinToString("") { "%02x".format(it) } + } else { + null + } + + if (peerID != null) { + // Log.v(TAG, "Found peerID $peerID in scan record for $deviceAddress") + if (connectionTracker.isPeerConnected(peerID)) { + Log.d(TAG, "Deduplication: Peer $peerID is already connected (ignoring $deviceAddress)") + return + } + } + // Log.d(TAG, "Received scan result from $deviceAddress - already connected: ${connectionTracker.isDeviceConnected(deviceAddress)}") // Store RSSI from scan results for later use (especially for server connections) @@ -332,7 +348,7 @@ class BluetoothGattClientManager( deviceName = device.name, deviceAddress = deviceAddress, rssi = rssi, - peerID = null // peerID unknown at scan time + peerID = peerID // Use the discovered peerID if available ) ) } catch (_: Exception) { } @@ -342,13 +358,12 @@ class BluetoothGattClientManager( Log.d(TAG, "Skipping device $deviceAddress due to weak signal: $rssi < ${powerManager.getRSSIThreshold()}") // Even if we skip connecting, still publish scan result to debug UI try { - val pid: String? = null // We don't know peerID until packet exchange DebugSettingsManager.getInstance().addScanResult( DebugScanResult( deviceName = device.name, deviceAddress = deviceAddress, rssi = rssi, - peerID = pid + peerID = peerID ) ) } catch (_: Exception) { } @@ -373,7 +388,7 @@ class BluetoothGattClientManager( // Add pending connection and start connection if (connectionTracker.addPendingConnection(deviceAddress)) { - connectToDevice(device, rssi) + connectToDevice(device, rssi, peerID) } } @@ -381,11 +396,11 @@ class BluetoothGattClientManager( * Connect to a device as GATT client */ @Suppress("DEPRECATION") - private fun connectToDevice(device: BluetoothDevice, rssi: Int) { + private fun connectToDevice(device: BluetoothDevice, rssi: Int, peerID: String? = null) { if (!permissionManager.hasBluetoothPermissions()) return val deviceAddress = device.address - Log.i(TAG, "Connecting to bitchat device: $deviceAddress") + Log.i(TAG, "Connecting to bitchat device: $deviceAddress (peerID: $peerID)") val gattCallback = object : BluetoothGattCallback() { override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { @@ -435,7 +450,8 @@ class BluetoothGattClientManager( device = gatt.device, gatt = gatt, rssi = rssi, - isClient = true + isClient = true, + peerID = peerID // Store the peerID discovered during scan ) connectionTracker.addDeviceConnection(deviceAddress, deviceConn) diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothGattServerManager.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothGattServerManager.kt index e06fd008..4a0630a2 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothGattServerManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothGattServerManager.kt @@ -24,7 +24,8 @@ class BluetoothGattServerManager( private val connectionTracker: BluetoothConnectionTracker, private val permissionManager: BluetoothPermissionManager, private val powerManager: PowerManager, - private val delegate: BluetoothConnectionManagerDelegate? + private val delegate: BluetoothConnectionManagerDelegate?, + private val myPeerID: String ) { companion object { @@ -372,13 +373,27 @@ class BluetoothGattServerManager( .setIncludeTxPowerLevel(false) .setIncludeDeviceName(false) .build() + + // Add stable identity (first 8 bytes of peerID) to Scan Response + // This allows scanners to deduplicate devices even if MAC address rotates + val peerIDBytes = try { + myPeerID.chunked(2).map { it.toInt(16).toByte() }.toByteArray().take(8).toByteArray() + } catch (e: Exception) { + ByteArray(0) + } + + val scanResponse = AdvertiseData.Builder() + .addServiceData(ParcelUuid(AppConstants.Mesh.Gatt.SERVICE_UUID), peerIDBytes) + .setIncludeTxPowerLevel(false) + .setIncludeDeviceName(false) + .build() advertiseCallback = object : AdvertiseCallback() { override fun onStartSuccess(settingsInEffect: AdvertiseSettings) { val mode = try { powerManager.getPowerInfo().split("Current Mode: ")[1].split("\n")[0] } catch (_: Exception) { "unknown" } - Log.i(TAG, "Advertising started (power mode: $mode)") + Log.i(TAG, "Advertising started (power mode: $mode) with stable ID: ${peerIDBytes.joinToString("") { "%02x".format(it) }}") } override fun onStartFailure(errorCode: Int) { @@ -387,7 +402,7 @@ class BluetoothGattServerManager( } try { - bleAdvertiser.startAdvertising(settings, data, advertiseCallback) + bleAdvertiser.startAdvertising(settings, data, scanResponse, advertiseCallback) } catch (se: SecurityException) { Log.e(TAG, "SecurityException starting advertising (missing permission?): ${se.message}") } catch (e: Exception) {