mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-24 22:45:20 +00:00
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
This commit is contained in:
@@ -85,10 +85,6 @@ class BluetoothConnectionManager(
|
|||||||
// Public property for address-peer mapping
|
// Public property for address-peer mapping
|
||||||
val addressPeerMap get() = connectionTracker.addressPeerMap
|
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 {
|
init {
|
||||||
powerManager.delegate = this
|
powerManager.delegate = this
|
||||||
// Observe debug settings to enforce role state while active
|
// Observe debug settings to enforce role state while active
|
||||||
|
|||||||
@@ -30,9 +30,6 @@ class BluetoothConnectionTracker(
|
|||||||
private val connectedDevices = ConcurrentHashMap<String, DeviceConnection>()
|
private val connectedDevices = ConcurrentHashMap<String, DeviceConnection>()
|
||||||
private val subscribedDevices = CopyOnWriteArrayList<BluetoothDevice>()
|
private val subscribedDevices = CopyOnWriteArrayList<BluetoothDevice>()
|
||||||
val addressPeerMap = ConcurrentHashMap<String, String>()
|
val addressPeerMap = ConcurrentHashMap<String, String>()
|
||||||
// Track whether we have seen the first ANNOUNCE on a given device connection
|
|
||||||
private val firstAnnounceSeen = ConcurrentHashMap<String, Boolean>()
|
|
||||||
|
|
||||||
// RSSI tracking from scan results (for devices we discover but may connect as servers)
|
// RSSI tracking from scan results (for devices we discover but may connect as servers)
|
||||||
private val scanRSSI = ConcurrentHashMap<String, Int>()
|
private val scanRSSI = ConcurrentHashMap<String, Int>()
|
||||||
|
|
||||||
@@ -94,8 +91,6 @@ class BluetoothConnectionTracker(
|
|||||||
Log.d(TAG, "Tracker: Adding device connection for $deviceAddress (isClient: ${deviceConn.isClient}")
|
Log.d(TAG, "Tracker: Adding device connection for $deviceAddress (isClient: ${deviceConn.isClient}")
|
||||||
connectedDevices[deviceAddress] = deviceConn
|
connectedDevices[deviceAddress] = deviceConn
|
||||||
pendingConnections.remove(deviceAddress)
|
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 }
|
subscribedDevices.removeAll { it.address == deviceAddress }
|
||||||
addressPeerMap.remove(deviceAddress)
|
addressPeerMap.remove(deviceAddress)
|
||||||
}
|
}
|
||||||
firstAnnounceSeen.remove(deviceAddress)
|
|
||||||
Log.d(TAG, "Cleaned up device connection for $deviceAddress")
|
Log.d(TAG, "Cleaned up device connection for $deviceAddress")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,21 +360,6 @@ class BluetoothConnectionTracker(
|
|||||||
addressPeerMap.clear()
|
addressPeerMap.clear()
|
||||||
pendingConnections.clear()
|
pendingConnections.clear()
|
||||||
scanRSSI.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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -111,6 +111,13 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
return signPacketBeforeBroadcast(packet)
|
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")
|
Log.d(TAG, "Delegates set up; GossipSyncManager initialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -469,21 +476,24 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
// Process the announce
|
// Process the announce
|
||||||
val isFirst = messageHandler.handleAnnounce(routed)
|
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 deviceAddress = routed.relayAddress
|
||||||
val pid = routed.peerID
|
val pid = routed.peerID
|
||||||
if (deviceAddress != null && pid != null) {
|
if (deviceAddress != null && pid != null) {
|
||||||
// First ANNOUNCE over a device connection defines a direct neighbor.
|
// Check if this is a direct connection (MAX TTL)
|
||||||
if (!connectionManager.hasSeenFirstAnnounce(deviceAddress)) {
|
// 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
|
// Bind or rebind this device address to the announcing peer
|
||||||
connectionManager.addressPeerMap[deviceAddress] = pid
|
connectionManager.addressPeerMap[deviceAddress] = pid
|
||||||
connectionManager.noteAnnounceReceived(deviceAddress)
|
Log.d(TAG, "Mapped device $deviceAddress to peer $pid (TTL=${routed.packet.ttl})")
|
||||||
Log.d(TAG, "Mapped device $deviceAddress to peer $pid on FIRST-ANNOUNCE for this connection")
|
|
||||||
|
|
||||||
// Mark as directly connected (upgrades from routed if needed)
|
// Mark as directly connected - refresh UI state
|
||||||
try { peerManager.setDirectConnection(pid, true) } catch (_: Exception) { }
|
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) { }
|
try { gossipSyncManager.scheduleInitialSyncToPeer(pid, 1_000) } catch (_: Exception) { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -583,12 +593,11 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
val peer = connectionManager.addressPeerMap[addr]
|
val peer = connectionManager.addressPeerMap[addr]
|
||||||
// ConnectionTracker has already removed the address mapping; be defensive either way
|
// ConnectionTracker has already removed the address mapping; be defensive either way
|
||||||
connectionManager.addressPeerMap.remove(addr)
|
connectionManager.addressPeerMap.remove(addr)
|
||||||
|
|
||||||
|
// refresh peer list on disconnect.
|
||||||
|
try { peerManager.refreshPeerList() } catch (_: Exception) { }
|
||||||
|
|
||||||
if (peer != null) {
|
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
|
// Verbose debug: device disconnected
|
||||||
try {
|
try {
|
||||||
val nick = peerManager.getPeerNickname(peer) ?: "unknown"
|
val nick = peerManager.getPeerNickname(peer) ?: "unknown"
|
||||||
|
|||||||
@@ -86,6 +86,9 @@ class PeerManager {
|
|||||||
// Delegate for callbacks
|
// Delegate for callbacks
|
||||||
var delegate: PeerManagerDelegate? = null
|
var delegate: PeerManagerDelegate? = null
|
||||||
|
|
||||||
|
// Callback to check if a peer is directly connected (injected by BluetoothMeshService)
|
||||||
|
var isPeerDirectlyConnected: ((String) -> Boolean)? = null
|
||||||
|
|
||||||
// Coroutines
|
// Coroutines
|
||||||
private val managerScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
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? {
|
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<String, PeerInfo> {
|
fun getVerifiedPeers(): Map<String, PeerInfo> {
|
||||||
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.
|
* Force a peer list update notification.
|
||||||
* Triggers a peer list update to refresh UI badges.
|
* Call this when connection state changes to refresh UI badges.
|
||||||
*/
|
*/
|
||||||
fun setDirectConnection(peerID: String, isDirect: Boolean) {
|
fun refreshPeerList() {
|
||||||
peers[peerID]?.let { existing ->
|
|
||||||
if (existing.isDirectConnection != isDirect) {
|
|
||||||
peers[peerID] = existing.copy(isDirectConnection = isDirect)
|
|
||||||
notifyPeerListUpdate()
|
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) { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Legacy Methods (maintained for compatibility)
|
// MARK: - Legacy Methods (maintained for compatibility)
|
||||||
@@ -364,7 +368,11 @@ class PeerManager {
|
|||||||
return buildString {
|
return buildString {
|
||||||
appendLine("=== Peer Manager Debug Info ===")
|
appendLine("=== Peer Manager Debug Info ===")
|
||||||
appendLine("Active Peers: ${activeIds.size}")
|
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 timeSince = (now - info.lastSeen) / 1000
|
||||||
val rssi = peerRSSI[peerID]?.let { "${it} dBm" } ?: "No RSSI"
|
val rssi = peerRSSI[peerID]?.let { "${it} dBm" } ?: "No RSSI"
|
||||||
val deviceAddress = addressPeerMap?.entries?.find { it.value == peerID }?.key
|
val deviceAddress = addressPeerMap?.entries?.find { it.value == peerID }?.key
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ Timers:
|
|||||||
- Added a `DeviceMonitoringManager` instance and provided a `disconnectCallback` that:
|
- Added a `DeviceMonitoringManager` instance and provided a `disconnectCallback` that:
|
||||||
- disconnects client GATT connections via `BluetoothConnectionTracker`.
|
- disconnects client GATT connections via `BluetoothConnectionTracker`.
|
||||||
- cancels server connections via `BluetoothGattServer.cancelConnection`.
|
- 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.
|
- Updated `componentDelegate.onPacketReceived` to notify per-device activity to the monitor.
|
||||||
|
|
||||||
2) GATT Client
|
2) GATT Client
|
||||||
@@ -59,7 +58,6 @@ Timers:
|
|||||||
|
|
||||||
4) ANNOUNCE Binding
|
4) ANNOUNCE Binding
|
||||||
- File: `BluetoothMeshService.kt` (in the ANNOUNCE handler where we first map device → peer)
|
- 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
|
## Behavior Summary
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user