mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-24 22:25:19 +00:00
drop unused connections
This commit is contained in:
@@ -57,6 +57,8 @@ class BluetoothConnectionManager(
|
||||
}
|
||||
|
||||
override fun onDeviceConnected(device: BluetoothDevice) {
|
||||
// Start ANNOUNCE watchdog generically for both client/server connections
|
||||
try { connectionTracker.startAnnounceWatchdog(device.address) } catch (_: Exception) { }
|
||||
delegate?.onDeviceConnected(device)
|
||||
}
|
||||
|
||||
@@ -87,6 +89,8 @@ class BluetoothConnectionManager(
|
||||
|
||||
init {
|
||||
powerManager.delegate = this
|
||||
// When tracker avoids a device, actively drop its connection (client or server)
|
||||
try { connectionTracker.setOnDeviceAvoided { addr, _ -> forceDisconnectAddress(addr, null) } } catch (_: Exception) { }
|
||||
// Observe debug settings to enforce role state while active
|
||||
try {
|
||||
val dbg = com.bitchat.android.ui.debug.DebugSettingsManager.getInstance()
|
||||
@@ -270,6 +274,26 @@ class BluetoothConnectionManager(
|
||||
}
|
||||
}
|
||||
|
||||
// Debug: avoided devices snapshot (address, reason, since)
|
||||
fun getAvoidedDevices(): List<Triple<String, String, Long>> {
|
||||
return try {
|
||||
val snap = connectionTracker.getAvoidedDevicesSnapshot()
|
||||
snap.map { (addr, info) -> Triple(addr, info.reason, info.since) }
|
||||
} catch (e: Exception) {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
// Debug: remove one entry from avoid list
|
||||
fun removeFromAvoidList(address: String) {
|
||||
try { connectionTracker.removeFromAvoidList(address) } catch (_: Exception) { }
|
||||
}
|
||||
|
||||
// Debug: clear the entire avoid list
|
||||
fun clearAvoidList() {
|
||||
try { connectionTracker.clearAvoidList() } catch (_: Exception) { }
|
||||
}
|
||||
|
||||
// Expose local adapter address for debug UI
|
||||
fun getLocalAdapterAddress(): String? = try { bluetoothAdapter?.address } catch (e: Exception) { null }
|
||||
|
||||
@@ -283,6 +307,36 @@ class BluetoothConnectionManager(
|
||||
fun connectToAddress(address: String): Boolean = clientManager.connectToAddress(address)
|
||||
fun disconnectAddress(address: String) { connectionTracker.disconnectDevice(address) }
|
||||
|
||||
// Disconnect a device whether it is a client or server connection; optionally add to avoid list
|
||||
fun forceDisconnectAddress(address: String, blacklistReason: String? = null) {
|
||||
try {
|
||||
// If client-side GATT exists, disconnect via GATT
|
||||
val dc = connectionTracker.getConnectedDevices()[address]
|
||||
if (dc?.gatt != null) {
|
||||
try { dc.gatt.disconnect() } catch (_: Exception) { }
|
||||
}
|
||||
// If it's a server-side subscribed device, cancel via GATT server
|
||||
val server = serverManager.getGattServer()
|
||||
if (server != null) {
|
||||
val sub = connectionTracker.getSubscribedDevices().firstOrNull { it.address == address }
|
||||
if (sub != null) {
|
||||
try { server.cancelConnection(sub) } catch (_: Exception) { }
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (blacklistReason != null) {
|
||||
try { connectionTracker.addToAvoidList(address, blacklistReason) } catch (_: Exception) { }
|
||||
} else {
|
||||
connectionTracker.cleanupDeviceConnection(address)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper for Mesh to acknowledge announce mapping and cancel watchdog
|
||||
fun markAnnounceReceived(deviceAddress: String, peerID: String) {
|
||||
try { connectionTracker.markAnnounceReceived(deviceAddress, peerID) } catch (_: Exception) { }
|
||||
}
|
||||
|
||||
|
||||
// Optionally disconnect all connections (server and client)
|
||||
fun disconnectAll() {
|
||||
|
||||
@@ -22,6 +22,7 @@ class BluetoothConnectionTracker(
|
||||
private const val TAG = "BluetoothConnectionTracker"
|
||||
private const val CONNECTION_RETRY_DELAY = 5000L
|
||||
private const val MAX_CONNECTION_ATTEMPTS = 3
|
||||
private const val DEFAULT_ANNOUNCE_TIMEOUT_MS = 5000L
|
||||
private const val CLEANUP_DELAY = 500L
|
||||
private const val CLEANUP_INTERVAL = 30000L // 30 seconds
|
||||
}
|
||||
@@ -36,9 +37,19 @@ class BluetoothConnectionTracker(
|
||||
|
||||
// Connection attempt tracking with automatic cleanup
|
||||
private val pendingConnections = ConcurrentHashMap<String, ConnectionAttempt>()
|
||||
// Track consecutive failures per device
|
||||
private val failureCounts = ConcurrentHashMap<String, Int>()
|
||||
// Devices to avoid reconnecting to (blacklist)
|
||||
private val avoidedDevices = ConcurrentHashMap<String, AvoidInfo>()
|
||||
// Announce watchdog jobs per device address
|
||||
private val announceWatchdogs = ConcurrentHashMap<String, kotlinx.coroutines.Job>()
|
||||
// Configurable ANNOUNCE timeout (ms)
|
||||
@Volatile private var announceTimeoutMs: Long = DEFAULT_ANNOUNCE_TIMEOUT_MS
|
||||
|
||||
// State management
|
||||
private var isActive = false
|
||||
// Optional callback to actively drop connections when a device is avoided
|
||||
@Volatile private var onDeviceAvoided: ((address: String, reason: String) -> Unit)? = null
|
||||
|
||||
/**
|
||||
* Consolidated device connection information
|
||||
@@ -66,6 +77,14 @@ class BluetoothConnectionTracker(
|
||||
attempts < MAX_CONNECTION_ATTEMPTS &&
|
||||
System.currentTimeMillis() - lastAttempt > CONNECTION_RETRY_DELAY
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata for avoided devices
|
||||
*/
|
||||
data class AvoidInfo(
|
||||
val reason: String,
|
||||
val since: Long = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
/**
|
||||
* Start the connection tracker
|
||||
@@ -91,6 +110,9 @@ class BluetoothConnectionTracker(
|
||||
Log.d(TAG, "Tracker: Adding device connection for $deviceAddress (isClient: ${deviceConn.isClient}")
|
||||
connectedDevices[deviceAddress] = deviceConn
|
||||
pendingConnections.remove(deviceAddress)
|
||||
// Successful connection -> reset failure count and cancel any blacklist for this address
|
||||
failureCounts.remove(deviceAddress)
|
||||
// Do not auto-remove from avoidedDevices to keep defensive policy; only manual removal
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -171,6 +193,11 @@ class BluetoothConnectionTracker(
|
||||
* Check if connection attempt is allowed
|
||||
*/
|
||||
fun isConnectionAttemptAllowed(deviceAddress: String): Boolean {
|
||||
// Never attempt to connect to avoided devices
|
||||
if (avoidedDevices.containsKey(deviceAddress)) {
|
||||
Log.d(TAG, "Tracker: Device $deviceAddress is in avoid list; skipping attempts")
|
||||
return false
|
||||
}
|
||||
val existingAttempt = pendingConnections[deviceAddress]
|
||||
return existingAttempt?.let {
|
||||
it.isExpired() || it.shouldRetry()
|
||||
@@ -218,6 +245,89 @@ class BluetoothConnectionTracker(
|
||||
fun removePendingConnection(deviceAddress: String) {
|
||||
pendingConnections.remove(deviceAddress)
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a connection failure. If failures exceed threshold, avoid device.
|
||||
*/
|
||||
fun recordConnectionFailure(deviceAddress: String, reason: String? = null) {
|
||||
val newCount = (failureCounts[deviceAddress] ?: 0) + 1
|
||||
failureCounts[deviceAddress] = newCount
|
||||
Log.w(TAG, "Tracker: Failure #$newCount for $deviceAddress${reason?.let { ": $it" } ?: ""}")
|
||||
if (newCount >= MAX_CONNECTION_ATTEMPTS) {
|
||||
addToAvoidList(deviceAddress, reason ?: "too_many_failures")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add device to avoid list and cleanup its connection state.
|
||||
*/
|
||||
fun addToAvoidList(deviceAddress: String, reason: String) {
|
||||
Log.w(TAG, "Tracker: Adding $deviceAddress to avoid list (reason: $reason)")
|
||||
avoidedDevices[deviceAddress] = AvoidInfo(reason)
|
||||
// Actively drop connection via callback if provided
|
||||
try { onDeviceAvoided?.invoke(deviceAddress, reason) } catch (_: Exception) { }
|
||||
// Ensure connection state is removed afterwards
|
||||
cleanupDeviceConnection(deviceAddress)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the ANNOUNCE timeout in milliseconds (configurable).
|
||||
*/
|
||||
fun setAnnounceTimeout(timeoutMs: Long) {
|
||||
announceTimeoutMs = timeoutMs.coerceAtLeast(1000L)
|
||||
}
|
||||
|
||||
/** Set a callback invoked when a device is added to avoid list. */
|
||||
fun setOnDeviceAvoided(callback: (address: String, reason: String) -> Unit) {
|
||||
onDeviceAvoided = callback
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a watchdog that drops the connection if ANNOUNCE not mapped for this address.
|
||||
*/
|
||||
fun startAnnounceWatchdog(deviceAddress: String) {
|
||||
// Cancel any existing watchdog first
|
||||
cancelAnnounceWatchdog(deviceAddress)
|
||||
val job = connectionScope.launch {
|
||||
val startedAt = System.currentTimeMillis()
|
||||
delay(announceTimeoutMs)
|
||||
// If no ANNOUNCE mapping exists and device still connected, drop and avoid
|
||||
val mapped = addressPeerMap.containsKey(deviceAddress)
|
||||
val stillConnected = connectedDevices.containsKey(deviceAddress) ||
|
||||
subscribedDevices.any { it.address == deviceAddress }
|
||||
if (!mapped && stillConnected) {
|
||||
Log.w(TAG, "Tracker: ANNOUNCE not received within ${announceTimeoutMs}ms for $deviceAddress; dropping + avoiding")
|
||||
addToAvoidList(deviceAddress, "announce_timeout")
|
||||
} else {
|
||||
Log.d(TAG, "Tracker: ANNOUNCE watchdog passed for $deviceAddress in ${System.currentTimeMillis() - startedAt}ms (mapped=$mapped, connected=$stillConnected)")
|
||||
}
|
||||
}
|
||||
announceWatchdogs[deviceAddress] = job
|
||||
}
|
||||
|
||||
/** Cancel ANNOUNCE watchdog for an address. */
|
||||
fun cancelAnnounceWatchdog(deviceAddress: String) {
|
||||
announceWatchdogs.remove(deviceAddress)?.cancel()
|
||||
}
|
||||
|
||||
/** Notify tracker that ANNOUNCE was received/mapped for this device. */
|
||||
fun markAnnounceReceived(deviceAddress: String, peerID: String) {
|
||||
// Map is updated externally; we just cancel watchdog here
|
||||
cancelAnnounceWatchdog(deviceAddress)
|
||||
Log.d(TAG, "Tracker: ANNOUNCE mapped for $deviceAddress -> $peerID; watchdog canceled")
|
||||
}
|
||||
|
||||
/** Remove a single device from the avoid list (allow future attempts). */
|
||||
fun removeFromAvoidList(deviceAddress: String) {
|
||||
avoidedDevices.remove(deviceAddress)
|
||||
Log.d(TAG, "Tracker: Removed $deviceAddress from avoid list")
|
||||
}
|
||||
|
||||
/** Clear the entire avoid list (dangerous; for debug UI). */
|
||||
fun clearAvoidList() {
|
||||
avoidedDevices.clear()
|
||||
Log.d(TAG, "Tracker: Cleared avoid list")
|
||||
}
|
||||
|
||||
/**
|
||||
* Get connected device count
|
||||
@@ -280,6 +390,7 @@ class BluetoothConnectionTracker(
|
||||
addressPeerMap.remove(deviceAddress)
|
||||
}
|
||||
pendingConnections.remove(deviceAddress)
|
||||
cancelAnnounceWatchdog(deviceAddress)
|
||||
Log.d(TAG, "Cleaned up device connection for $deviceAddress")
|
||||
}
|
||||
|
||||
@@ -313,6 +424,10 @@ class BluetoothConnectionTracker(
|
||||
addressPeerMap.clear()
|
||||
pendingConnections.clear()
|
||||
scanRSSI.clear()
|
||||
failureCounts.clear()
|
||||
avoidedDevices.clear()
|
||||
announceWatchdogs.values.forEach { it.cancel() }
|
||||
announceWatchdogs.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -337,7 +452,7 @@ class BluetoothConnectionTracker(
|
||||
|
||||
// Log current state
|
||||
Log.d(TAG, "Periodic cleanup: ${connectedDevices.size} connections, ${pendingConnections.size} pending")
|
||||
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Error in periodic cleanup: ${e.message}")
|
||||
}
|
||||
@@ -365,10 +480,26 @@ class BluetoothConnectionTracker(
|
||||
appendLine(" - $address: ${attempt.attempts} attempts, last ${elapsed}s ago")
|
||||
}
|
||||
appendLine()
|
||||
appendLine("Failures: ${failureCounts.size}")
|
||||
failureCounts.forEach { (address, count) ->
|
||||
appendLine(" - $address: $count failures")
|
||||
}
|
||||
appendLine()
|
||||
appendLine("Avoid List: ${avoidedDevices.size}")
|
||||
avoidedDevices.forEach { (address, info) ->
|
||||
val age = (now - info.since) / 1000
|
||||
appendLine(" - $address: ${info.reason} (${age}s)")
|
||||
}
|
||||
appendLine()
|
||||
appendLine("Scan RSSI Cache: ${scanRSSI.size}")
|
||||
scanRSSI.forEach { (address, rssi) ->
|
||||
appendLine(" - $address: $rssi dBm")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Snapshot of avoided devices for debug UI */
|
||||
fun getAvoidedDevicesSnapshot(): Map<String, AvoidInfo> {
|
||||
return avoidedDevices.toMap()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -410,6 +410,8 @@ class BluetoothGattClientManager(
|
||||
if (status == 147) {
|
||||
Log.e(TAG, "Client: Connection establishment failed (status 147) for $deviceAddress")
|
||||
}
|
||||
// Record failure for retry policy / blacklist
|
||||
connectionTracker.recordConnectionFailure(deviceAddress, "gatt_status_$status")
|
||||
} else {
|
||||
Log.d(TAG, "Client: Cleanly disconnected from $deviceAddress")
|
||||
connectionTracker.cleanupDeviceConnection(deviceAddress)
|
||||
|
||||
@@ -359,8 +359,26 @@ class BluetoothMeshService(private val context: Context) {
|
||||
// Only set mapping if not already mapped
|
||||
if (!connectionManager.addressPeerMap.containsKey(deviceAddress)) {
|
||||
connectionManager.addressPeerMap[deviceAddress] = pid
|
||||
// Cancel watchdog and mark announce received
|
||||
connectionManager.markAnnounceReceived(deviceAddress, pid)
|
||||
Log.d(TAG, "Mapped device $deviceAddress to peer $pid on ANNOUNCE")
|
||||
|
||||
// If we already have connections for this peer on other devices, drop the older ones
|
||||
try {
|
||||
val duplicates = connectionManager.addressPeerMap
|
||||
.filter { (addr, mappedPid) -> mappedPid == pid && addr != deviceAddress }
|
||||
.map { it.key }
|
||||
if (duplicates.isNotEmpty()) {
|
||||
Log.w(TAG, "Duplicate connections for peer $pid on devices: $duplicates — dropping old ones")
|
||||
duplicates.forEach { otherAddr ->
|
||||
// Blacklist the old devices to avoid reconnect storm
|
||||
connectionManager.forceDisconnectAddress(otherAddr, "duplicate_peer_$pid")
|
||||
// Remove mapping explicitly
|
||||
connectionManager.addressPeerMap.remove(otherAddr)
|
||||
}
|
||||
}
|
||||
} catch (_: Exception) { }
|
||||
|
||||
// Mark this peer as directly connected for UI
|
||||
try {
|
||||
peerManager.getPeerInfo(pid)?.let {
|
||||
|
||||
@@ -48,6 +48,7 @@ fun DebugSettingsSheet(
|
||||
val scanResults by manager.scanResults.collectAsState()
|
||||
val connectedDevices by manager.connectedDevices.collectAsState()
|
||||
val relayStats by manager.relayStats.collectAsState()
|
||||
var avoided by remember { mutableStateOf(listOf<Triple<String, String, Long>>()) }
|
||||
|
||||
// Push live connected devices from mesh service whenever sheet is visible
|
||||
LaunchedEffect(isPresented) {
|
||||
@@ -71,6 +72,7 @@ fun DebugSettingsSheet(
|
||||
)
|
||||
}
|
||||
manager.updateConnectedDevices(devices)
|
||||
avoided = meshService.connectionManager.getAvoidedDevices()
|
||||
kotlinx.coroutines.delay(1000)
|
||||
}
|
||||
}
|
||||
@@ -313,6 +315,47 @@ fun DebugSettingsSheet(
|
||||
}
|
||||
}
|
||||
|
||||
// Avoid list (under scan results)
|
||||
item {
|
||||
Surface(shape = RoundedCornerShape(12.dp), color = colorScheme.surfaceVariant.copy(alpha = 0.2f)) {
|
||||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Icon(Icons.Filled.Cancel, contentDescription = null, tint = Color(0xFFBF1A1A))
|
||||
Text("avoid list", fontFamily = FontFamily.Monospace, fontSize = 14.sp, fontWeight = FontWeight.Medium)
|
||||
Spacer(Modifier.weight(1f))
|
||||
Text(
|
||||
"clear",
|
||||
color = Color(0xFFBF1A1A),
|
||||
fontFamily = FontFamily.Monospace,
|
||||
modifier = Modifier.clickable { meshService.connectionManager.clearAvoidList() }
|
||||
)
|
||||
}
|
||||
if (avoided.isEmpty()) {
|
||||
Text("none", fontFamily = FontFamily.Monospace, fontSize = 11.sp, color = colorScheme.onSurface.copy(alpha = 0.6f))
|
||||
} else {
|
||||
val now = System.currentTimeMillis()
|
||||
avoided.sortedByDescending { it.third }.forEach { (addr, reason, since) ->
|
||||
val ageSec = ((now - since) / 1000).coerceAtLeast(0)
|
||||
Surface(shape = RoundedCornerShape(8.dp), color = colorScheme.surface.copy(alpha = 0.6f)) {
|
||||
Row(Modifier.fillMaxWidth().padding(10.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(addr, fontFamily = FontFamily.Monospace, fontSize = 12.sp)
|
||||
Text("reason: $reason • ${ageSec}s", fontFamily = FontFamily.Monospace, fontSize = 11.sp, color = colorScheme.onSurface.copy(alpha = 0.7f))
|
||||
}
|
||||
Text(
|
||||
"remove",
|
||||
color = Color(0xFF007AFF),
|
||||
fontFamily = FontFamily.Monospace,
|
||||
modifier = Modifier.clickable { meshService.connectionManager.removeFromAvoidList(addr) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Debug console
|
||||
item {
|
||||
Surface(shape = RoundedCornerShape(12.dp), color = colorScheme.surfaceVariant.copy(alpha = 0.2f)) {
|
||||
|
||||
Reference in New Issue
Block a user