From 1ae75d84b5b93bfaebec7fce051215f95b38c4ac Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 14 Sep 2025 14:28:20 +0200 Subject: [PATCH] connection manager init --- .../mesh/BluetoothConnectionManager.kt | 51 +++++- .../mesh/BluetoothConnectionTracker.kt | 10 +- .../mesh/BluetoothGattClientManager.kt | 42 ++++- .../mesh/BluetoothGattServerManager.kt | 20 ++- .../android/mesh/BluetoothMeshService.kt | 4 + .../android/mesh/DeviceMonitoringManager.kt | 169 ++++++++++++++++++ 6 files changed, 290 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/com/bitchat/android/mesh/DeviceMonitoringManager.kt 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 19c103a3..44a0eea0 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt @@ -38,6 +38,7 @@ class BluetoothConnectionManager( private val permissionManager = BluetoothPermissionManager(context) private val connectionTracker = BluetoothConnectionTracker(connectionScope, powerManager) private val packetBroadcaster = BluetoothPacketBroadcaster(connectionScope, connectionTracker, fragmentManager) + private val deviceMonitor = DeviceMonitoringManager(connectionScope) // Delegate for component managers to call back to main manager private val componentDelegate = object : BluetoothConnectionManagerDelegate { @@ -49,6 +50,8 @@ class BluetoothConnectionManager( if (currentRSSI != null) { delegate?.onRSSIUpdated(bluetoothDevice.address, currentRSSI) } + // Update monitor activity timer + try { deviceMonitor.onAnyPacketReceived(bluetoothDevice.address) } catch (_: Exception) { } } if (peerID == myPeerID) return // Ignore messages from self @@ -70,10 +73,10 @@ class BluetoothConnectionManager( } private val serverManager = BluetoothGattServerManager( - context, connectionScope, connectionTracker, permissionManager, powerManager, componentDelegate + context, connectionScope, connectionTracker, permissionManager, powerManager, componentDelegate, deviceMonitor ) private val clientManager = BluetoothGattClientManager( - context, connectionScope, connectionTracker, permissionManager, powerManager, componentDelegate + context, connectionScope, connectionTracker, permissionManager, powerManager, componentDelegate, deviceMonitor ) // Service state @@ -87,6 +90,20 @@ class BluetoothConnectionManager( init { 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 try { val dbg = com.bitchat.android.ui.debug.DebugSettingsManager.getInstance() @@ -296,6 +313,36 @@ class BluetoothConnectionManager( fun connectToAddress(address: String): Boolean = clientManager.connectToAddress(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) fun disconnectAll() { 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 3494d1b2..ca0ce7fd 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionTracker.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionTracker.kt @@ -88,7 +88,7 @@ class BluetoothConnectionTracker( * Add a device connection */ 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 pendingConnections.remove(deviceAddress) } @@ -314,6 +314,14 @@ class BluetoothConnectionTracker( pendingConnections.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 diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothGattClientManager.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothGattClientManager.kt index bd36c8ce..fe983610 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothGattClientManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothGattClientManager.kt @@ -26,7 +26,8 @@ class BluetoothGattClientManager( private val connectionTracker: BluetoothConnectionTracker, private val permissionManager: BluetoothPermissionManager, private val powerManager: PowerManager, - private val delegate: BluetoothConnectionManagerDelegate? + private val delegate: BluetoothConnectionManagerDelegate?, + private val deviceMonitor: DeviceMonitoringManager ) { companion object { @@ -52,6 +53,15 @@ class BluetoothGattClientManager( fun connectToAddress(deviceAddress: String): Boolean { val device = bluetoothAdapter?.getRemoteDevice(deviceAddress) 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 connectToDevice(device, rssi) true @@ -319,13 +329,24 @@ class BluetoothGattClientManager( val rssi = result.rssi val deviceAddress = device.address val scanRecord = result.scanRecord - + // CRITICAL: Only process devices that have our service UUID val hasOurService = scanRecord?.serviceUuids?.any { it.uuid == SERVICE_UUID } == true if (!hasOurService) { 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)}") // Store RSSI from scan results for later use (especially for server connections) @@ -392,6 +413,16 @@ class BluetoothGattClientManager( val deviceAddress = device.address 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() { override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { @@ -415,6 +446,9 @@ class BluetoothGattClientManager( connectionTracker.cleanupDeviceConnection(deviceAddress) } + // Record disconnect for monitoring (error bursts -> block) + deviceMonitor.onDeviceDisconnected(deviceAddress, status) + // Notify higher layers about device disconnection to update direct flags delegate?.onDeviceDisconnected(gatt.device) @@ -475,6 +509,8 @@ class BluetoothGattClientManager( connectionScope.launch { delay(200) Log.i(TAG, "Client: Connection setup complete for $deviceAddress") + // Start monitoring timers for this connection + deviceMonitor.onConnectionEstablished(deviceAddress) delegate?.onDeviceConnected(device) } } else { @@ -502,6 +538,8 @@ class BluetoothGattClientManager( if (packet != null) { val peerID = packet.senderID.take(8).toByteArray().joinToString("") { "%02x".format(it) } 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) } else { Log.w(TAG, "Client: Failed to parse packet from ${gatt.device.address}, size: ${value.size} bytes") diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothGattServerManager.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothGattServerManager.kt index 5be14307..4e94cd92 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothGattServerManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothGattServerManager.kt @@ -23,7 +23,8 @@ class BluetoothGattServerManager( private val connectionTracker: BluetoothConnectionTracker, private val permissionManager: BluetoothPermissionManager, private val powerManager: PowerManager, - private val delegate: BluetoothConnectionManagerDelegate? + private val delegate: BluetoothConnectionManagerDelegate?, + private val deviceMonitor: DeviceMonitoringManager ) { companion object { @@ -167,6 +168,17 @@ class BluetoothGattServerManager( when (newState) { BluetoothProfile.STATE_CONNECTED -> { 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) val rssi = connectionTracker.getBestRSSI(device.address) ?: Int.MIN_VALUE @@ -181,6 +193,8 @@ class BluetoothGattServerManager( connectionScope.launch { delay(1000) if (isActive) { // Check if still active + // Start monitoring timers for this connection + deviceMonitor.onConnectionEstablished(device.address) delegate?.onDeviceConnected(device) } } @@ -188,6 +202,7 @@ class BluetoothGattServerManager( BluetoothProfile.STATE_DISCONNECTED -> { Log.i(TAG, "Server: Device disconnected ${device.address}") connectionTracker.cleanupDeviceConnection(device.address) + deviceMonitor.onDeviceDisconnected(device.address, status) // Notify delegate about device disconnection so higher layers can update direct flags delegate?.onDeviceDisconnected(device) } @@ -229,6 +244,8 @@ class BluetoothGattServerManager( if (packet != null) { val peerID = packet.senderID.take(8).toByteArray().joinToString("") { "%02x".format(it) } Log.d(TAG, "Server: Parsed packet type ${packet.type} from $peerID") + // Update per-device activity + deviceMonitor.onAnyPacketReceived(device.address) delegate?.onPacketReceived(packet, peerID, device) } else { Log.w(TAG, "Server: Failed to parse packet from ${device.address}, size: ${value.size} bytes") @@ -263,6 +280,7 @@ class BluetoothGattServerManager( connectionScope.launch { delay(100) if (isActive) { // Check if still active + deviceMonitor.onConnectionEstablished(device.address) delegate?.onDeviceConnected(device) } } 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 8d6b357f..e5153223 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt @@ -405,6 +405,8 @@ class BluetoothMeshService(private val context: Context) { if (!connectionManager.addressPeerMap.containsKey(deviceAddress)) { connectionManager.addressPeerMap[deviceAddress] = pid 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 try { @@ -1034,6 +1036,8 @@ class BluetoothMeshService(private val context: Context) { securityManager.clearAllData() peerManager.clearAllPeers() peerManager.clearAllFingerprints() + // Also clear device monitoring + connection tracking + try { connectionManager.clearDeviceMonitoringAndTracking() } catch (_: Exception) { } Log.d(TAG, "โœ… Cleared all mesh service internal data") } catch (e: Exception) { Log.e(TAG, "โŒ Error clearing mesh service internal data: ${e.message}") diff --git a/app/src/main/java/com/bitchat/android/mesh/DeviceMonitoringManager.kt b/app/src/main/java/com/bitchat/android/mesh/DeviceMonitoringManager.kt new file mode 100644 index 00000000..17522d31 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/mesh/DeviceMonitoringManager.kt @@ -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() + + // Per-device timers and state + private val announceTimers = ConcurrentHashMap() + private val inactivityTimers = ConcurrentHashMap() + private val lastPacketAt = ConcurrentHashMap() + + // Disconnect error history per device + private val errorHistory = ConcurrentHashMap>() + + // 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") + ) + } +}