mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 01:25:21 +00:00
connection manager init
This commit is contained in:
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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")
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user