Files
bitchat-android/app/src/main/java/com/bitchat/android/mesh/PeerManager.kt
T
2025-07-08 23:38:35 +02:00

258 lines
7.1 KiB
Kotlin

package com.bitchat.android.mesh
import android.util.Log
import com.bitchat.android.model.BitchatMessage
import kotlinx.coroutines.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArrayList
/**
* Manages active peers, nicknames, and RSSI tracking
* Extracted from BluetoothMeshService for better separation of concerns
*/
class PeerManager {
companion object {
private const val TAG = "PeerManager"
private const val STALE_PEER_TIMEOUT = 180000L // 3 minutes (same as iOS)
private const val CLEANUP_INTERVAL = 60000L // 1 minute
}
// Peer tracking data
private val peerNicknames = ConcurrentHashMap<String, String>()
private val activePeers = ConcurrentHashMap<String, Long>() // peerID -> lastSeen timestamp
private val peerRSSI = ConcurrentHashMap<String, Int>()
private val announcedPeers = CopyOnWriteArrayList<String>()
private val announcedToPeers = CopyOnWriteArrayList<String>()
// Delegate for callbacks
var delegate: PeerManagerDelegate? = null
// Coroutines
private val managerScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
init {
startPeriodicCleanup()
}
/**
* Update peer last seen timestamp
*/
fun updatePeerLastSeen(peerID: String) {
if (peerID != "unknown") {
activePeers[peerID] = System.currentTimeMillis()
}
}
/**
* Add or update peer with nickname
*/
fun addOrUpdatePeer(peerID: String, nickname: String): Boolean {
if (peerID == "unknown") return false
// Clean up stale peer IDs with the same nickname (exact same logic as iOS)
val stalePeerIDs = mutableListOf<String>()
peerNicknames.forEach { (existingPeerID, existingNickname) ->
if (existingNickname == nickname && existingPeerID != peerID) {
val lastSeen = activePeers[existingPeerID] ?: 0
val wasRecentlySeen = (System.currentTimeMillis() - lastSeen) < 10000
if (!wasRecentlySeen) {
stalePeerIDs.add(existingPeerID)
}
}
}
// Remove stale peer IDs
stalePeerIDs.forEach { stalePeerID ->
removePeer(stalePeerID, notifyDelegate = false)
}
// Check if this is a new peer announcement
val isFirstAnnounce = !announcedPeers.contains(peerID)
// Update peer data
peerNicknames[peerID] = nickname
activePeers[peerID] = System.currentTimeMillis()
// Handle first announcement
if (isFirstAnnounce) {
announcedPeers.add(peerID)
delegate?.onPeerConnected(nickname)
notifyPeerListUpdate()
return true
}
return false
}
/**
* Remove peer
*/
fun removePeer(peerID: String, notifyDelegate: Boolean = true) {
val nickname = peerNicknames.remove(peerID)
activePeers.remove(peerID)
peerRSSI.remove(peerID)
announcedPeers.remove(peerID)
announcedToPeers.remove(peerID)
if (notifyDelegate && nickname != null) {
delegate?.onPeerDisconnected(nickname)
notifyPeerListUpdate()
}
}
/**
* Update peer RSSI
*/
fun updatePeerRSSI(peerID: String, rssi: Int) {
if (peerID != "unknown") {
peerRSSI[peerID] = rssi
}
}
/**
* Check if peer has been announced to
*/
fun hasAnnouncedToPeer(peerID: String): Boolean {
return announcedToPeers.contains(peerID)
}
/**
* Mark peer as announced to
*/
fun markPeerAsAnnouncedTo(peerID: String) {
if (!announcedToPeers.contains(peerID)) {
announcedToPeers.add(peerID)
}
}
/**
* Check if peer is active
*/
fun isPeerActive(peerID: String): Boolean {
return activePeers.containsKey(peerID)
}
/**
* Get peer nickname
*/
fun getPeerNickname(peerID: String): String? {
return peerNicknames[peerID]
}
/**
* Get all peer nicknames
*/
fun getAllPeerNicknames(): Map<String, String> {
return peerNicknames.toMap()
}
/**
* Get all peer RSSI values
*/
fun getAllPeerRSSI(): Map<String, Int> {
return peerRSSI.toMap()
}
/**
* Get list of active peer IDs
*/
fun getActivePeerIDs(): List<String> {
return activePeers.keys.toList().sorted()
}
/**
* Get active peer count
*/
fun getActivePeerCount(): Int {
return activePeers.size
}
/**
* Clear all peer data
*/
fun clearAllPeers() {
peerNicknames.clear()
activePeers.clear()
peerRSSI.clear()
announcedPeers.clear()
announcedToPeers.clear()
notifyPeerListUpdate()
}
/**
* Get debug information
*/
fun getDebugInfo(): String {
return buildString {
appendLine("=== Peer Manager Debug Info ===")
appendLine("Active Peers: ${activePeers.size}")
activePeers.forEach { (peerID, lastSeen) ->
val nickname = peerNicknames[peerID] ?: "Unknown"
val timeSince = (System.currentTimeMillis() - lastSeen) / 1000
val rssi = peerRSSI[peerID]?.let { "${it} dBm" } ?: "No RSSI"
appendLine(" - $peerID ($nickname) - last seen ${timeSince}s ago, RSSI: $rssi")
}
appendLine("Announced Peers: ${announcedPeers.size}")
appendLine("Announced To Peers: ${announcedToPeers.size}")
}
}
/**
* Notify delegate of peer list updates
*/
private fun notifyPeerListUpdate() {
val peerList = getActivePeerIDs()
delegate?.onPeerListUpdated(peerList)
}
/**
* Start periodic cleanup of stale peers
*/
private fun startPeriodicCleanup() {
managerScope.launch {
while (isActive) {
delay(CLEANUP_INTERVAL)
cleanupStalePeers()
}
}
}
/**
* Clean up stale peers (same 3-minute threshold as iOS)
*/
private fun cleanupStalePeers() {
val now = System.currentTimeMillis()
val peersToRemove = activePeers.entries.filter { (_, lastSeen) ->
now - lastSeen > STALE_PEER_TIMEOUT
}.map { it.key }
peersToRemove.forEach { peerID ->
Log.d(TAG, "Removing stale peer: $peerID")
removePeer(peerID)
}
if (peersToRemove.isNotEmpty()) {
Log.d(TAG, "Cleaned up ${peersToRemove.size} stale peers")
}
}
/**
* Shutdown the manager
*/
fun shutdown() {
managerScope.cancel()
clearAllPeers()
}
}
/**
* Delegate interface for peer manager callbacks
*/
interface PeerManagerDelegate {
fun onPeerConnected(nickname: String)
fun onPeerDisconnected(nickname: String)
fun onPeerListUpdated(peerIDs: List<String>)
}