connection manager init

This commit is contained in:
callebtc
2025-09-14 14:28:20 +02:00
parent df4aa4b25d
commit 1ae75d84b5
6 changed files with 290 additions and 6 deletions
@@ -38,6 +38,7 @@ class BluetoothConnectionManager(
private val permissionManager = BluetoothPermissionManager(context) private val permissionManager = BluetoothPermissionManager(context)
private val connectionTracker = BluetoothConnectionTracker(connectionScope, powerManager) private val connectionTracker = BluetoothConnectionTracker(connectionScope, powerManager)
private val packetBroadcaster = BluetoothPacketBroadcaster(connectionScope, connectionTracker, fragmentManager) private val packetBroadcaster = BluetoothPacketBroadcaster(connectionScope, connectionTracker, fragmentManager)
private val deviceMonitor = DeviceMonitoringManager(connectionScope)
// Delegate for component managers to call back to main manager // Delegate for component managers to call back to main manager
private val componentDelegate = object : BluetoothConnectionManagerDelegate { private val componentDelegate = object : BluetoothConnectionManagerDelegate {
@@ -49,6 +50,8 @@ class BluetoothConnectionManager(
if (currentRSSI != null) { if (currentRSSI != null) {
delegate?.onRSSIUpdated(bluetoothDevice.address, currentRSSI) delegate?.onRSSIUpdated(bluetoothDevice.address, currentRSSI)
} }
// Update monitor activity timer
try { deviceMonitor.onAnyPacketReceived(bluetoothDevice.address) } catch (_: Exception) { }
} }
if (peerID == myPeerID) return // Ignore messages from self if (peerID == myPeerID) return // Ignore messages from self
@@ -70,10 +73,10 @@ class BluetoothConnectionManager(
} }
private val serverManager = BluetoothGattServerManager( private val serverManager = BluetoothGattServerManager(
context, connectionScope, connectionTracker, permissionManager, powerManager, componentDelegate context, connectionScope, connectionTracker, permissionManager, powerManager, componentDelegate, deviceMonitor
) )
private val clientManager = BluetoothGattClientManager( private val clientManager = BluetoothGattClientManager(
context, connectionScope, connectionTracker, permissionManager, powerManager, componentDelegate context, connectionScope, connectionTracker, permissionManager, powerManager, componentDelegate, deviceMonitor
) )
// Service state // Service state
@@ -87,6 +90,20 @@ class BluetoothConnectionManager(
init { init {
powerManager.delegate = this powerManager.delegate = this
// Provide disconnect callback for monitor to drop either client or server connections
deviceMonitor.setDisconnectCallback { address ->
try {
// Try client disconnect first
connectionTracker.disconnectDevice(address)
} catch (_: Exception) { }
try {
// Then cancel any server-side connection
val dev = connectionTracker.getSubscribedDevices().firstOrNull { it.address == address }
serverManager.getGattServer()?.let { gs -> dev?.let { d -> gs.cancelConnection(d) } }
} catch (_: Exception) { }
// Ensure tracker cleanup
try { connectionTracker.cleanupDeviceConnection(address) } catch (_: Exception) { }
}
// Observe debug settings to enforce role state while active // Observe debug settings to enforce role state while active
try { try {
val dbg = com.bitchat.android.ui.debug.DebugSettingsManager.getInstance() val dbg = com.bitchat.android.ui.debug.DebugSettingsManager.getInstance()
@@ -296,6 +313,36 @@ class BluetoothConnectionManager(
fun connectToAddress(address: String): Boolean = clientManager.connectToAddress(address) fun connectToAddress(address: String): Boolean = clientManager.connectToAddress(address)
fun disconnectAddress(address: String) { connectionTracker.disconnectDevice(address) } fun disconnectAddress(address: String) { connectionTracker.disconnectDevice(address) }
// Notify monitor when we see a verified announce on a device
fun noteAnnounceReceived(address: String) { deviceMonitor.onAnnounceReceived(address) }
// Clear blocklist + monitoring timers and all connection tracking (panic triple-tap)
fun clearDeviceMonitoringAndTracking() {
try {
// Attempt to disconnect active client connections
connectionTracker.getConnectedDevices().values.forEach { dc ->
try { dc.gatt?.disconnect() } catch (_: Exception) { }
}
} catch (_: Exception) { }
try {
// Attempt to cancel server-side connections
val gs = serverManager.getGattServer()
connectionTracker.getSubscribedDevices().forEach { d ->
try { gs?.cancelConnection(d) } catch (_: Exception) { }
}
} catch (_: Exception) { }
try { deviceMonitor.clearAll() } catch (_: Exception) { }
try { connectionTracker.clearAllTracking() } catch (_: Exception) { }
try {
com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().addDebugMessage(
com.bitchat.android.ui.debug.DebugMessage.SystemMessage("🧹 Cleared device blocklist + tracking (panic)")
)
} catch (_: Exception) { }
}
// Optionally disconnect all connections (server and client) // Optionally disconnect all connections (server and client)
fun disconnectAll() { fun disconnectAll() {
@@ -88,7 +88,7 @@ class BluetoothConnectionTracker(
* Add a device connection * Add a device connection
*/ */
fun addDeviceConnection(deviceAddress: String, deviceConn: DeviceConnection) { fun addDeviceConnection(deviceAddress: String, deviceConn: DeviceConnection) {
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)
} }
@@ -314,6 +314,14 @@ class BluetoothConnectionTracker(
pendingConnections.clear() pendingConnections.clear()
scanRSSI.clear() scanRSSI.clear()
} }
/**
* Public: Clear all tracking state without affecting PowerManager or timers
*/
fun clearAllTracking() {
clearAllConnections()
Log.w(TAG, "Tracker: Cleared all device tracking state")
}
/** /**
* Start periodic cleanup of expired connections * Start periodic cleanup of expired connections
@@ -26,7 +26,8 @@ class BluetoothGattClientManager(
private val connectionTracker: BluetoothConnectionTracker, private val connectionTracker: BluetoothConnectionTracker,
private val permissionManager: BluetoothPermissionManager, private val permissionManager: BluetoothPermissionManager,
private val powerManager: PowerManager, private val powerManager: PowerManager,
private val delegate: BluetoothConnectionManagerDelegate? private val delegate: BluetoothConnectionManagerDelegate?,
private val deviceMonitor: DeviceMonitoringManager
) { ) {
companion object { companion object {
@@ -52,6 +53,15 @@ class BluetoothGattClientManager(
fun connectToAddress(deviceAddress: String): Boolean { fun connectToAddress(deviceAddress: String): Boolean {
val device = bluetoothAdapter?.getRemoteDevice(deviceAddress) val device = bluetoothAdapter?.getRemoteDevice(deviceAddress)
return if (device != null) { return if (device != null) {
if (deviceMonitor.isBlocked(device.address)) {
Log.w(TAG, "connectToAddress: Blocked device ${device.address}, denying connection")
try {
com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().addDebugMessage(
com.bitchat.android.ui.debug.DebugMessage.SystemMessage("⛔ Denied outgoing connect to blocked ${device.address}")
)
} catch (_: Exception) { }
return false
}
val rssi = connectionTracker.getBestRSSI(deviceAddress) ?: -50 val rssi = connectionTracker.getBestRSSI(deviceAddress) ?: -50
connectToDevice(device, rssi) connectToDevice(device, rssi)
true true
@@ -319,13 +329,24 @@ class BluetoothGattClientManager(
val rssi = result.rssi val rssi = result.rssi
val deviceAddress = device.address val deviceAddress = device.address
val scanRecord = result.scanRecord val scanRecord = result.scanRecord
// CRITICAL: Only process devices that have our service UUID // CRITICAL: Only process devices that have our service UUID
val hasOurService = scanRecord?.serviceUuids?.any { it.uuid == SERVICE_UUID } == true val hasOurService = scanRecord?.serviceUuids?.any { it.uuid == SERVICE_UUID } == true
if (!hasOurService) { if (!hasOurService) {
return return
} }
// Deny connection attempts to blocked devices
if (deviceMonitor.isBlocked(deviceAddress)) {
Log.d(TAG, "Skipping blocked device $deviceAddress from scan result")
try {
com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().addDebugMessage(
com.bitchat.android.ui.debug.DebugMessage.SystemMessage("⛔ Skipping blocked device in scan: $deviceAddress")
)
} catch (_: Exception) { }
return
}
// Log.d(TAG, "Received scan result from $deviceAddress - already connected: ${connectionTracker.isDeviceConnected(deviceAddress)}") // 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) // Store RSSI from scan results for later use (especially for server connections)
@@ -392,6 +413,16 @@ class BluetoothGattClientManager(
val deviceAddress = device.address val deviceAddress = device.address
Log.i(TAG, "Connecting to bitchat device: $deviceAddress") Log.i(TAG, "Connecting to bitchat device: $deviceAddress")
if (deviceMonitor.isBlocked(deviceAddress)) {
Log.w(TAG, "Blocked device $deviceAddress — aborting client connect")
try {
com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().addDebugMessage(
com.bitchat.android.ui.debug.DebugMessage.SystemMessage("⛔ Aborting client connect to blocked $deviceAddress")
)
} catch (_: Exception) { }
return
}
val gattCallback = object : BluetoothGattCallback() { val gattCallback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
@@ -415,6 +446,9 @@ class BluetoothGattClientManager(
connectionTracker.cleanupDeviceConnection(deviceAddress) connectionTracker.cleanupDeviceConnection(deviceAddress)
} }
// Record disconnect for monitoring (error bursts -> block)
deviceMonitor.onDeviceDisconnected(deviceAddress, status)
// Notify higher layers about device disconnection to update direct flags // Notify higher layers about device disconnection to update direct flags
delegate?.onDeviceDisconnected(gatt.device) delegate?.onDeviceDisconnected(gatt.device)
@@ -475,6 +509,8 @@ class BluetoothGattClientManager(
connectionScope.launch { connectionScope.launch {
delay(200) delay(200)
Log.i(TAG, "Client: Connection setup complete for $deviceAddress") Log.i(TAG, "Client: Connection setup complete for $deviceAddress")
// Start monitoring timers for this connection
deviceMonitor.onConnectionEstablished(deviceAddress)
delegate?.onDeviceConnected(device) delegate?.onDeviceConnected(device)
} }
} else { } else {
@@ -502,6 +538,8 @@ class BluetoothGattClientManager(
if (packet != null) { if (packet != null) {
val peerID = packet.senderID.take(8).toByteArray().joinToString("") { "%02x".format(it) } val peerID = packet.senderID.take(8).toByteArray().joinToString("") { "%02x".format(it) }
Log.d(TAG, "Client: Parsed packet type ${packet.type} from $peerID") Log.d(TAG, "Client: Parsed packet type ${packet.type} from $peerID")
// Update per-device activity
deviceMonitor.onAnyPacketReceived(gatt.device.address)
delegate?.onPacketReceived(packet, peerID, gatt.device) delegate?.onPacketReceived(packet, peerID, gatt.device)
} else { } else {
Log.w(TAG, "Client: Failed to parse packet from ${gatt.device.address}, size: ${value.size} bytes") Log.w(TAG, "Client: Failed to parse packet from ${gatt.device.address}, size: ${value.size} bytes")
@@ -23,7 +23,8 @@ class BluetoothGattServerManager(
private val connectionTracker: BluetoothConnectionTracker, private val connectionTracker: BluetoothConnectionTracker,
private val permissionManager: BluetoothPermissionManager, private val permissionManager: BluetoothPermissionManager,
private val powerManager: PowerManager, private val powerManager: PowerManager,
private val delegate: BluetoothConnectionManagerDelegate? private val delegate: BluetoothConnectionManagerDelegate?,
private val deviceMonitor: DeviceMonitoringManager
) { ) {
companion object { companion object {
@@ -167,6 +168,17 @@ class BluetoothGattServerManager(
when (newState) { when (newState) {
BluetoothProfile.STATE_CONNECTED -> { BluetoothProfile.STATE_CONNECTED -> {
Log.i(TAG, "Server: Device connected ${device.address}") Log.i(TAG, "Server: Device connected ${device.address}")
// Deny immediately if blocked
if (deviceMonitor.isBlocked(device.address)) {
Log.w(TAG, "Server: Blocked device ${device.address} attempted to connect — canceling")
try {
com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().addDebugMessage(
com.bitchat.android.ui.debug.DebugMessage.SystemMessage("⛔ Denied incoming connection from blocked ${device.address}")
)
} catch (_: Exception) { }
try { gattServer?.cancelConnection(device) } catch (_: Exception) { }
return
}
// Get best available RSSI (scan RSSI for server connections) // Get best available RSSI (scan RSSI for server connections)
val rssi = connectionTracker.getBestRSSI(device.address) ?: Int.MIN_VALUE val rssi = connectionTracker.getBestRSSI(device.address) ?: Int.MIN_VALUE
@@ -181,6 +193,8 @@ class BluetoothGattServerManager(
connectionScope.launch { connectionScope.launch {
delay(1000) delay(1000)
if (isActive) { // Check if still active if (isActive) { // Check if still active
// Start monitoring timers for this connection
deviceMonitor.onConnectionEstablished(device.address)
delegate?.onDeviceConnected(device) delegate?.onDeviceConnected(device)
} }
} }
@@ -188,6 +202,7 @@ class BluetoothGattServerManager(
BluetoothProfile.STATE_DISCONNECTED -> { BluetoothProfile.STATE_DISCONNECTED -> {
Log.i(TAG, "Server: Device disconnected ${device.address}") Log.i(TAG, "Server: Device disconnected ${device.address}")
connectionTracker.cleanupDeviceConnection(device.address) connectionTracker.cleanupDeviceConnection(device.address)
deviceMonitor.onDeviceDisconnected(device.address, status)
// Notify delegate about device disconnection so higher layers can update direct flags // Notify delegate about device disconnection so higher layers can update direct flags
delegate?.onDeviceDisconnected(device) delegate?.onDeviceDisconnected(device)
} }
@@ -229,6 +244,8 @@ class BluetoothGattServerManager(
if (packet != null) { if (packet != null) {
val peerID = packet.senderID.take(8).toByteArray().joinToString("") { "%02x".format(it) } val peerID = packet.senderID.take(8).toByteArray().joinToString("") { "%02x".format(it) }
Log.d(TAG, "Server: Parsed packet type ${packet.type} from $peerID") Log.d(TAG, "Server: Parsed packet type ${packet.type} from $peerID")
// Update per-device activity
deviceMonitor.onAnyPacketReceived(device.address)
delegate?.onPacketReceived(packet, peerID, device) delegate?.onPacketReceived(packet, peerID, device)
} else { } else {
Log.w(TAG, "Server: Failed to parse packet from ${device.address}, size: ${value.size} bytes") Log.w(TAG, "Server: Failed to parse packet from ${device.address}, size: ${value.size} bytes")
@@ -263,6 +280,7 @@ class BluetoothGattServerManager(
connectionScope.launch { connectionScope.launch {
delay(100) delay(100)
if (isActive) { // Check if still active if (isActive) { // Check if still active
deviceMonitor.onConnectionEstablished(device.address)
delegate?.onDeviceConnected(device) delegate?.onDeviceConnected(device)
} }
} }
@@ -405,6 +405,8 @@ class BluetoothMeshService(private val context: Context) {
if (!connectionManager.addressPeerMap.containsKey(deviceAddress)) { if (!connectionManager.addressPeerMap.containsKey(deviceAddress)) {
connectionManager.addressPeerMap[deviceAddress] = pid connectionManager.addressPeerMap[deviceAddress] = pid
Log.d(TAG, "Mapped device $deviceAddress to peer $pid on ANNOUNCE") Log.d(TAG, "Mapped device $deviceAddress to peer $pid on ANNOUNCE")
// Let monitor know first ANNOUNCE was seen for this device
try { connectionManager.noteAnnounceReceived(deviceAddress) } catch (_: Exception) { }
// Mark this peer as directly connected for UI // Mark this peer as directly connected for UI
try { try {
@@ -1034,6 +1036,8 @@ class BluetoothMeshService(private val context: Context) {
securityManager.clearAllData() securityManager.clearAllData()
peerManager.clearAllPeers() peerManager.clearAllPeers()
peerManager.clearAllFingerprints() peerManager.clearAllFingerprints()
// Also clear device monitoring + connection tracking
try { connectionManager.clearDeviceMonitoringAndTracking() } catch (_: Exception) { }
Log.d(TAG, "✅ Cleared all mesh service internal data") Log.d(TAG, "✅ Cleared all mesh service internal data")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "❌ Error clearing mesh service internal data: ${e.message}") Log.e(TAG, "❌ Error clearing mesh service internal data: ${e.message}")
@@ -0,0 +1,169 @@
package com.bitchat.android.mesh
import android.bluetooth.BluetoothGatt
import android.util.Log
import kotlinx.coroutines.*
import java.util.concurrent.ConcurrentHashMap
/**
* Centralized per-device monitoring and blocklist management.
* - Blocks devices by MAC and auto-unblocks after a TTL
* - Drops connections that don't ANNOUNCE within 15s
* - Drops connections after 60s of packet inactivity
* - Blocks devices with >=5 error disconnects within 5 minutes
*/
class DeviceMonitoringManager(
private val scope: CoroutineScope
) {
companion object {
private const val TAG = "DeviceMonitoringManager"
private const val ANNOUNCE_TIMEOUT_MS = 15_000L
private const val INACTIVITY_TIMEOUT_MS = 60_000L
private const val BLOCK_DURATION_MS = 15 * 60_000L
private const val ERROR_WINDOW_MS = 5 * 60_000L
private const val ERROR_THRESHOLD = 5
}
// Debug manager for chat-visible logs (guarded)
private val debugManager by lazy { try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance() } catch (e: Exception) { null } }
// Blocked devices with unblock-at timestamp
private val blocked = ConcurrentHashMap<String, Long>()
// Per-device timers and state
private val announceTimers = ConcurrentHashMap<String, Job>()
private val inactivityTimers = ConcurrentHashMap<String, Job>()
private val lastPacketAt = ConcurrentHashMap<String, Long>()
// Disconnect error history per device
private val errorHistory = ConcurrentHashMap<String, MutableList<Long>>()
// Callback to actually disconnect/close a device connection
private var disconnectCallback: ((String) -> Unit)? = null
fun setDisconnectCallback(cb: (address: String) -> Unit) {
disconnectCallback = cb
}
fun isBlocked(address: String): Boolean {
val expiry = blocked[address] ?: return false
if (System.currentTimeMillis() > expiry) {
blocked.remove(address)
return false
}
return true
}
fun block(address: String, reason: String) {
val until = System.currentTimeMillis() + BLOCK_DURATION_MS
blocked[address] = until
Log.w(TAG, "Blocked $address for 15m: $reason")
debugManager?.addDebugMessage(
com.bitchat.android.ui.debug.DebugMessage.SystemMessage("⛔ Blocked $address for 15m — $reason")
)
// Best-effort disconnect now
disconnectCallback?.invoke(address)
// Schedule auto-unblock
scope.launch {
delay(BLOCK_DURATION_MS)
blocked.remove(address)
Log.d(TAG, "Auto-unblocked $address after TTL")
debugManager?.addDebugMessage(
com.bitchat.android.ui.debug.DebugMessage.SystemMessage("✅ Auto-unblocked $address after 15m")
)
}
}
fun onConnectionEstablished(address: String) {
if (isBlocked(address)) {
// Safety: drop immediately if somehow connected while blocked
block(address, "Connected while blocked; dropping")
return
}
// Start ANNOUNCE timer
announceTimers.remove(address)?.cancel()
announceTimers[address] = scope.launch {
delay(ANNOUNCE_TIMEOUT_MS)
// If timer fires and still no announce, block
Log.w(TAG, "No ANNOUNCE within 15s for $address — dropping + blocking")
block(address, "No ANNOUNCE within 15s")
}
debugManager?.addDebugMessage(
com.bitchat.android.ui.debug.DebugMessage.SystemMessage("📡 Monitoring $address — waiting for first ANNOUNCE (15s)")
)
// Start inactivity timer immediately so 60s without any packet also triggers
scheduleInactivityTimer(address, System.currentTimeMillis())
}
fun onAnnounceReceived(address: String) {
announceTimers.remove(address)?.cancel()
Log.d(TAG, "First ANNOUNCE received on $address; cancel announce timer")
debugManager?.addDebugMessage(
com.bitchat.android.ui.debug.DebugMessage.SystemMessage("🆗 First ANNOUNCE received on $address — continuing")
)
}
fun onAnyPacketReceived(address: String) {
val now = System.currentTimeMillis()
lastPacketAt[address] = now
scheduleInactivityTimer(address, now)
// Avoid spamming chat per-packet; rely on inactivity timer logs
}
private fun scheduleInactivityTimer(address: String, since: Long) {
inactivityTimers.remove(address)?.cancel()
inactivityTimers[address] = scope.launch {
val now = System.currentTimeMillis()
val elapsed = now - since
val wait = if (elapsed >= INACTIVITY_TIMEOUT_MS) 0L else (INACTIVITY_TIMEOUT_MS - elapsed)
delay(wait)
val last = lastPacketAt[address] ?: since
if (System.currentTimeMillis() - last >= INACTIVITY_TIMEOUT_MS) {
Log.w(TAG, "No packets for >60s from $address — dropping + blocking")
block(address, "Inactivity >60s")
}
}
debugManager?.addDebugMessage(
com.bitchat.android.ui.debug.DebugMessage.SystemMessage("⏳ Inactivity timer set for $address (60s)")
)
}
fun onDeviceDisconnected(address: String, status: Int) {
// Cancel timers on disconnect
announceTimers.remove(address)?.cancel()
inactivityTimers.remove(address)?.cancel()
if (status == BluetoothGatt.GATT_SUCCESS) return
val now = System.currentTimeMillis()
val list = errorHistory.getOrPut(address) { mutableListOf() }
// Drop entries older than window
val cutoff = now - ERROR_WINDOW_MS
list.removeAll { it < cutoff }
list.add(now)
if (list.size >= ERROR_THRESHOLD) {
Log.w(TAG, "$address reached $ERROR_THRESHOLD error disconnects in 5m — blocking")
block(address, ">=5 error disconnects in 5m")
list.clear()
}
}
fun clearForAddress(address: String) {
announceTimers.remove(address)?.cancel()
inactivityTimers.remove(address)?.cancel()
lastPacketAt.remove(address)
}
fun clearAll() {
announceTimers.values.forEach { it.cancel() }
inactivityTimers.values.forEach { it.cancel() }
announceTimers.clear()
inactivityTimers.clear()
lastPacketAt.clear()
blocked.clear()
errorHistory.clear()
Log.w(TAG, "Cleared all device monitoring state + blocklist")
debugManager?.addDebugMessage(
com.bitchat.android.ui.debug.DebugMessage.SystemMessage("🗑️ Cleared blocklist and device monitoring state")
)
}
}