From 83fbcca557c7d43bb2f356f89250d7d08c12d8ea Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Thu, 15 Jan 2026 15:24:24 +0700 Subject: [PATCH] Implement iOS-compatible Direct Peer Detection (TTL Logic) (#574) * Clean up direct peer detection logic (Max TTL) and remove unrelated routing/media changes * cleanup fix --- .../mesh/BluetoothConnectionManager.kt | 4 -- .../mesh/BluetoothConnectionTracker.kt | 21 --------- .../android/mesh/BluetoothMeshService.kt | 35 ++++++++------ .../com/bitchat/android/mesh/PeerManager.kt | 46 +++++++++++-------- docs/device_manager.md | 2 - 5 files changed, 49 insertions(+), 59 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 cf5975b2..b982f199 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt @@ -85,10 +85,6 @@ class BluetoothConnectionManager( // Public property for address-peer mapping val addressPeerMap get() = connectionTracker.addressPeerMap - // Expose first-announce helpers to higher layers - fun noteAnnounceReceived(address: String) { connectionTracker.noteAnnounceReceived(address) } - fun hasSeenFirstAnnounce(address: String): Boolean = connectionTracker.hasSeenFirstAnnounce(address) - init { powerManager.delegate = this // Observe debug settings to enforce role state while active 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 7500be21..59fca7aa 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionTracker.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionTracker.kt @@ -30,9 +30,6 @@ class BluetoothConnectionTracker( private val connectedDevices = ConcurrentHashMap() private val subscribedDevices = CopyOnWriteArrayList() val addressPeerMap = ConcurrentHashMap() - // Track whether we have seen the first ANNOUNCE on a given device connection - private val firstAnnounceSeen = ConcurrentHashMap() - // RSSI tracking from scan results (for devices we discover but may connect as servers) private val scanRSSI = ConcurrentHashMap() @@ -94,8 +91,6 @@ class BluetoothConnectionTracker( Log.d(TAG, "Tracker: Adding device connection for $deviceAddress (isClient: ${deviceConn.isClient}") connectedDevices[deviceAddress] = deviceConn pendingConnections.remove(deviceAddress) - // Mark as awaiting first ANNOUNCE on this connection - firstAnnounceSeen[deviceAddress] = false } /** @@ -332,7 +327,6 @@ class BluetoothConnectionTracker( subscribedDevices.removeAll { it.address == deviceAddress } addressPeerMap.remove(deviceAddress) } - firstAnnounceSeen.remove(deviceAddress) Log.d(TAG, "Cleaned up device connection for $deviceAddress") } @@ -366,23 +360,8 @@ class BluetoothConnectionTracker( addressPeerMap.clear() pendingConnections.clear() scanRSSI.clear() - firstAnnounceSeen.clear() } - /** - * Mark that we have received the first ANNOUNCE over this device connection. - */ - fun noteAnnounceReceived(deviceAddress: String) { - firstAnnounceSeen[deviceAddress] = true - } - - /** - * Check whether the first ANNOUNCE has been seen for a device connection. - */ - fun hasSeenFirstAnnounce(deviceAddress: String): Boolean { - return firstAnnounceSeen[deviceAddress] == true - } - /** * Start periodic cleanup of expired connections */ 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 a0ce15c3..474c04a5 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt @@ -111,6 +111,13 @@ class BluetoothMeshService(private val context: Context) { return signPacketBeforeBroadcast(packet) } } + + // Inject dynamic direct connection check into PeerManager + // Matches iOS logic: checks if we have an active hardware mapping for this peer + peerManager.isPeerDirectlyConnected = { peerID -> + connectionManager.addressPeerMap.containsValue(peerID) + } + Log.d(TAG, "Delegates set up; GossipSyncManager initialized") } @@ -469,21 +476,24 @@ class BluetoothMeshService(private val context: Context) { // Process the announce val isFirst = messageHandler.handleAnnounce(routed) - // Map device address -> peerID on first announce seen over this device connection + // Map device address -> peerID based on TTL (max TTL = direct neighbor) + // Matches iOS logic: any announce with max TTL on a link defines the direct peer val deviceAddress = routed.relayAddress val pid = routed.peerID if (deviceAddress != null && pid != null) { - // First ANNOUNCE over a device connection defines a direct neighbor. - if (!connectionManager.hasSeenFirstAnnounce(deviceAddress)) { + // Check if this is a direct connection (MAX TTL) + // Note: packet.ttl is UByte, compare with AppConstants.MESSAGE_TTL_HOPS + val isDirect = routed.packet.ttl == com.bitchat.android.util.AppConstants.MESSAGE_TTL_HOPS + + if (isDirect) { // 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") + Log.d(TAG, "Mapped device $deviceAddress to peer $pid (TTL=${routed.packet.ttl})") - // Mark as directly connected (upgrades from routed if needed) - try { peerManager.setDirectConnection(pid, true) } catch (_: Exception) { } + // Mark as directly connected - refresh UI state + try { peerManager.refreshPeerList() } catch (_: Exception) { } - // Initial sync for this newly direct peer + // Initial sync for this direct peer try { gossipSyncManager.scheduleInitialSyncToPeer(pid, 1_000) } catch (_: Exception) { } } } @@ -583,12 +593,11 @@ class BluetoothMeshService(private val context: Context) { val peer = connectionManager.addressPeerMap[addr] // ConnectionTracker has already removed the address mapping; be defensive either way connectionManager.addressPeerMap.remove(addr) + + // refresh peer list on disconnect. + try { peerManager.refreshPeerList() } catch (_: Exception) { } + if (peer != null) { - val stillMapped = connectionManager.addressPeerMap.values.any { it == peer } - if (!stillMapped) { - // Peer might still be reachable indirectly; mark as not-direct - try { peerManager.setDirectConnection(peer, false) } catch (_: Exception) { } - } // Verbose debug: device disconnected try { val nick = peerManager.getPeerNickname(peer) ?: "unknown" diff --git a/app/src/main/java/com/bitchat/android/mesh/PeerManager.kt b/app/src/main/java/com/bitchat/android/mesh/PeerManager.kt index d4c84c03..536a9b2a 100644 --- a/app/src/main/java/com/bitchat/android/mesh/PeerManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/PeerManager.kt @@ -86,6 +86,9 @@ class PeerManager { // Delegate for callbacks var delegate: PeerManagerDelegate? = null + // Callback to check if a peer is directly connected (injected by BluetoothMeshService) + var isPeerDirectlyConnected: ((String) -> Boolean)? = null + // Coroutines private val managerScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) @@ -145,10 +148,18 @@ class PeerManager { } /** - * Get peer info + * Get peer info with dynamic direct connection status */ fun getPeerInfo(peerID: String): PeerInfo? { - return peers[peerID] + return peers[peerID]?.let { info -> + // Dynamically check direct connection status from ConnectionManager + val isDirect = isPeerDirectlyConnected?.invoke(peerID) ?: false + if (info.isDirectConnection != isDirect) { + info.copy(isDirectConnection = isDirect) + } else { + info + } + } } /** @@ -159,28 +170,21 @@ class PeerManager { } /** - * Get all verified peers + * Get all verified peers with dynamic direct connection status */ fun getVerifiedPeers(): Map { - return peers.filterValues { it.isVerifiedNickname } + return peers.filterValues { it.isVerifiedNickname }.mapValues { (_, info) -> + val isDirect = isPeerDirectlyConnected?.invoke(info.id) ?: false + if (info.isDirectConnection != isDirect) info.copy(isDirectConnection = isDirect) else info + } } /** - * Set whether a peer is directly connected over Bluetooth. - * Triggers a peer list update to refresh UI badges. + * Force a peer list update notification. + * Call this when connection state changes to refresh UI badges. */ - fun setDirectConnection(peerID: String, isDirect: Boolean) { - peers[peerID]?.let { existing -> - if (existing.isDirectConnection != isDirect) { - peers[peerID] = existing.copy(isDirectConnection = isDirect) - notifyPeerListUpdate() - // NEW: notify UI state (if available via delegate path) about directness change - try { - // Best-effort: delegate path flows up to ChatViewModel via didUpdatePeerList - // No direct reference to UI layer here by design. - } catch (_: Exception) { } - } - } + fun refreshPeerList() { + notifyPeerListUpdate() } // MARK: - Legacy Methods (maintained for compatibility) @@ -364,7 +368,11 @@ class PeerManager { return buildString { appendLine("=== Peer Manager Debug Info ===") appendLine("Active Peers: ${activeIds.size}") - peers.forEach { (peerID, info) -> + peers.forEach { (peerID, storedInfo) -> + // Use dynamic direct status for debug log accuracy + val isDirect = isPeerDirectlyConnected?.invoke(peerID) ?: false + val info = if (storedInfo.isDirectConnection != isDirect) storedInfo.copy(isDirectConnection = isDirect) else storedInfo + val timeSince = (now - info.lastSeen) / 1000 val rssi = peerRSSI[peerID]?.let { "${it} dBm" } ?: "No RSSI" val deviceAddress = addressPeerMap?.entries?.find { it.value == peerID }?.key diff --git a/docs/device_manager.md b/docs/device_manager.md index 092403b0..c42cd2c5 100644 --- a/docs/device_manager.md +++ b/docs/device_manager.md @@ -38,7 +38,6 @@ Timers: - Added a `DeviceMonitoringManager` instance and provided a `disconnectCallback` that: - disconnects client GATT connections via `BluetoothConnectionTracker`. - cancels server connections via `BluetoothGattServer.cancelConnection`. -- Exposed `noteAnnounceReceived(address)` as a small helper for higher layers. - Updated `componentDelegate.onPacketReceived` to notify per-device activity to the monitor. 2) GATT Client @@ -59,7 +58,6 @@ Timers: 4) ANNOUNCE Binding - File: `BluetoothMeshService.kt` (in the ANNOUNCE handler where we first map device → peer) -- After mapping a device address to a peer on first verified ANNOUNCE, call `connectionManager.noteAnnounceReceived(address)` to cancel the 15s timer for that device. ## Behavior Summary