bitchat-android/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt
2026-07-27 18:44:49 +02:00

518 lines
18 KiB
Kotlin

package com.bitchat.android.mesh
import android.bluetooth.*
import android.content.Context
import android.util.Log
import com.bitchat.android.model.RoutedPacket
import com.bitchat.android.protocol.BitchatPacket
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
/**
* Power-optimized Bluetooth connection manager with comprehensive memory management
* Integrates with PowerManager for adaptive power consumption
* Coordinates smaller, focused components for better maintainability
*/
class BluetoothConnectionManager(
private val context: Context,
private val myPeerID: String,
private val fragmentManager: FragmentManager? = null
) : PowerManagerDelegate {
companion object {
private const val TAG = "BluetoothConnectionManager"
}
// Core Bluetooth components
private val bluetoothManager: BluetoothManager =
context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
private val bluetoothAdapter: BluetoothAdapter? = bluetoothManager.adapter
// Power management
private val powerManager = PowerManager(context.applicationContext)
// Coroutines
private val connectionScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
// Component managers
private val permissionManager = BluetoothPermissionManager(context)
private val connectionTracker = BluetoothConnectionTracker(connectionScope, powerManager)
private val packetBroadcaster = BluetoothPacketBroadcaster(connectionScope, connectionTracker, fragmentManager, myPeerID)
// Delegate for component managers to call back to main manager
private val componentDelegate = object : BluetoothConnectionManagerDelegate {
override fun onPacketReceived(
packet: BitchatPacket,
peerID: String,
device: BluetoothDevice?,
ingressLinkID: String
) {
device?.let { bluetoothDevice ->
// Get current RSSI for this device and update if available
val currentRSSI = connectionTracker.getBestRSSI(bluetoothDevice.address)
if (currentRSSI != null) {
delegate?.onRSSIUpdated(bluetoothDevice.address, currentRSSI)
}
}
if (peerID == myPeerID) return // Ignore messages from self
delegate?.onPacketReceived(packet, peerID, device, ingressLinkID)
}
override fun onDeviceConnected(device: BluetoothDevice) {
// Trigger limit enforcement immediately upon any new connection
enforceStrictLimits()
delegate?.onDeviceConnected(device)
}
override fun onDeviceDisconnected(device: BluetoothDevice, linkID: String?) {
delegate?.onDeviceDisconnected(device, linkID)
}
override fun onRSSIUpdated(deviceAddress: String, rssi: Int) {
delegate?.onRSSIUpdated(deviceAddress, rssi)
}
}
private val serverManager = BluetoothGattServerManager(
context, connectionScope, connectionTracker, permissionManager, powerManager, componentDelegate, myPeerID
)
private val clientManager = BluetoothGattClientManager(
context, connectionScope, connectionTracker, permissionManager, powerManager, componentDelegate
)
// Service state
private var isActive = false
// Delegate for callbacks
var delegate: BluetoothConnectionManagerDelegate? = null
// Public property for address-peer mapping
val addressPeerMap get() = connectionTracker.addressPeerMap
fun observePeerIfCurrent(deviceAddress: String, linkID: String, peerID: String): Boolean =
connectionTracker.observePeerIfCurrent(deviceAddress, linkID, peerID)
fun getCurrentLinkID(deviceAddress: String): String? =
connectionTracker.getCurrentLinkID(deviceAddress)
private fun isBleTransportEnabled(): Boolean {
return try {
com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().bleEnabled.value
} catch (_: Exception) {
try { com.bitchat.android.ui.debug.DebugPreferenceManager.getBleEnabled(true) } catch (_: Exception) { true }
}
}
private fun isGattServerEnabled(): Boolean {
return isBleTransportEnabled() &&
(try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().gattServerEnabled.value } catch (_: Exception) { true })
}
private fun isGattClientEnabled(): Boolean {
return isBleTransportEnabled() &&
(try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().gattClientEnabled.value } catch (_: Exception) { true })
}
init {
powerManager.delegate = this
// Observe debug settings to enforce role state while active
try {
val dbg = com.bitchat.android.ui.debug.DebugSettingsManager.getInstance()
// Master transport enable/disable
connectionScope.launch {
dbg.bleEnabled.collect { enabled ->
if (enabled) return@collect
if (isActive) {
disableTransport()
}
}
}
// Role enable/disable
connectionScope.launch {
dbg.gattServerEnabled.collect { enabled ->
if (!isActive) return@collect
if (enabled && isBleTransportEnabled()) startServer() else stopServer()
}
}
connectionScope.launch {
dbg.gattClientEnabled.collect { enabled ->
if (!isActive) return@collect
if (enabled && isBleTransportEnabled()) startClient() else stopClient()
}
}
// Centralized limit enforcement on any setting change
connectionScope.launch {
combine(
dbg.maxConnectionsOverall,
dbg.maxServerConnections,
dbg.maxClientConnections
) { _, _, _ ->
// We don't need the values here, we just need to trigger enforcement
Unit
}.collect {
if (isActive) {
enforceStrictLimits()
}
}
}
} catch (_: Exception) { }
}
/**
* Centralized connection limit enforcement
*/
private fun enforceStrictLimits() {
if (!isActive) return
try {
val dbg = com.bitchat.android.ui.debug.DebugSettingsManager.getInstance()
val maxOverall = dbg.maxConnectionsOverall.value
val maxServer = dbg.maxServerConnections.value
val maxClient = dbg.maxClientConnections.value
// Get list of connections to evict to satisfy all constraints
val toEvict = connectionTracker.getConnectionsToEvict(maxOverall, maxServer, maxClient)
if (toEvict.isNotEmpty()) {
Log.i(TAG, "Enforcing limits (max: $maxOverall, s: $maxServer, c: $maxClient) - evicting ${toEvict.size} connections")
toEvict.forEach { conn ->
if (conn.isClient) {
try { conn.gatt?.disconnect() } catch (_: Exception) { }
} else {
serverManager.disconnectDevice(conn.device)
}
}
}
} catch (e: Exception) {
Log.e(TAG, "Error enforcing limits: ${e.message}")
}
}
/**
* Start all Bluetooth services with power optimization
*/
fun startServices(): Boolean {
if (!isBleTransportEnabled()) {
Log.i(TAG, "BLE transport disabled by debug settings; not starting Bluetooth services")
disableTransport()
return false
}
if (!permissionManager.hasBluetoothPermissions()) {
Log.e(TAG, "Missing Bluetooth permissions")
return false
}
if (bluetoothAdapter?.isEnabled != true) {
Log.e(TAG, "Bluetooth is not enabled")
return false
}
try {
isActive = true
// set the adapter's name to our 8-character peerID for iOS privacy, TODO: Make this configurable
// try {
// if (bluetoothAdapter?.name != myPeerID) {
// bluetoothAdapter?.name = myPeerID
// Log.d(TAG, "Set Bluetooth adapter name to peerID: $myPeerID for iOS compatibility.")
// }
// } catch (se: SecurityException) {
// Log.e(TAG, "Missing BLUETOOTH_CONNECT permission to set adapter name.", se)
// }
// Start all component managers
connectionScope.launch {
// Start connection tracker first
connectionTracker.start()
// Start power manager
powerManager.start()
// Start server/client based on debug settings
val startServer = isGattServerEnabled()
val startClient = isGattClientEnabled()
if (startServer) {
if (!serverManager.start()) {
Log.e(TAG, "Failed to start server manager")
this@BluetoothConnectionManager.isActive = false
return@launch
}
} else {
Log.i(TAG, "GATT Server disabled by debug settings; not starting")
}
if (startClient) {
if (!clientManager.start()) {
Log.e(TAG, "Failed to start client manager")
this@BluetoothConnectionManager.isActive = false
return@launch
}
} else {
Log.i(TAG, "GATT Client disabled by debug settings; not starting")
}
Log.i(TAG, "Bluetooth services started successfully")
}
return true
} catch (e: Exception) {
Log.e(TAG, "Failed to start Bluetooth services: ${e.message}")
isActive = false
return false
}
}
/**
* Disable BLE without cancelling this manager's coroutine scope, so it can be re-enabled.
*/
fun disableTransport() {
Log.i(TAG, "Disabling BLE transport")
isActive = false
connectionScope.launch {
clientManager.stop()
serverManager.stop()
connectionTracker.stop()
}
}
/**
* Stop all Bluetooth services with proper cleanup
*/
fun stopServices() {
isActive = false
connectionScope.launch {
// Stop component managers
clientManager.stop()
serverManager.stop()
// Stop power manager
powerManager.stop()
// Stop connection tracker
connectionTracker.stop()
// Cancel the coroutine scope
connectionScope.cancel()
Log.i(TAG, "All Bluetooth services stopped")
}
}
/**
* Indicates whether this instance can be safely reused for a future start.
* Returns false if its coroutine scope has been cancelled.
*/
fun isReusable(): Boolean {
return connectionScope.isActive
}
/**
* Broadcast packet to connected devices with connection limit enforcement
* Automatically fragments large packets to fit within BLE MTU limits
*/
fun broadcastPacket(routed: RoutedPacket): Boolean {
if (!isActive || !isBleTransportEnabled()) return false
return packetBroadcaster.broadcastPacket(
routed,
serverManager.getGattServer(),
serverManager.getCharacteristic()
)
}
fun sendToPeer(peerID: String, routed: RoutedPacket): Boolean {
if (!isActive || !isBleTransportEnabled()) return false
return packetBroadcaster.sendToPeer(
peerID,
routed,
serverManager.getGattServer(),
serverManager.getCharacteristic()
)
}
fun cancelTransfer(transferId: String): Boolean {
return packetBroadcaster.cancelTransfer(transferId)
}
/**
* Send a packet directly to a specific peer, without broadcasting to others.
*/
fun sendPacketToPeer(peerID: String, packet: BitchatPacket): Boolean {
if (!isActive || !isBleTransportEnabled()) return false
return packetBroadcaster.sendPacketToPeer(
RoutedPacket(packet),
peerID,
serverManager.getGattServer(),
serverManager.getCharacteristic()
)
}
fun sendPacketToLink(deviceAddress: String, linkID: String, packet: BitchatPacket): Boolean {
if (!isActive || !isBleTransportEnabled()) return false
return packetBroadcaster.sendPacketToLink(
RoutedPacket(packet),
deviceAddress,
linkID,
serverManager.getGattServer(),
serverManager.getCharacteristic()
)
}
// Expose role controls for debug UI
fun startServer() {
if (!isActive || !isBleTransportEnabled()) return
connectionScope.launch { if (isGattServerEnabled()) serverManager.start() }
}
fun stopServer() { connectionScope.launch { serverManager.stop() } }
fun startClient() {
if (!isActive || !isBleTransportEnabled()) return
connectionScope.launch { if (isGattClientEnabled()) clientManager.start() }
}
fun stopClient() { connectionScope.launch { clientManager.stop() } }
// Inject nickname resolver for broadcaster logs
fun setNicknameResolver(resolver: (String) -> String?) { packetBroadcaster.setNicknameResolver(resolver) }
// Debug snapshots for connected devices
fun getConnectedDeviceEntries(): List<Triple<String, Boolean, Int?>> {
return try {
connectionTracker.getConnectedDevices().values.map { dc ->
val rssi = if (dc.rssi != Int.MIN_VALUE) dc.rssi else null
Triple(dc.device.address, dc.isClient, rssi)
}
} catch (e: Exception) {
emptyList()
}
}
// Expose local adapter address for debug UI
fun getLocalAdapterAddress(): String? = try { bluetoothAdapter?.address } catch (e: Exception) { null }
fun isClientConnection(address: String): Boolean? {
return try { connectionTracker.getConnectedDevices()[address]?.isClient } catch (e: Exception) { null }
}
/**
* Public: connect/disconnect helpers for debug UI
*/
fun connectToAddress(address: String): Boolean {
if (!isActive || !isBleTransportEnabled()) return false
return clientManager.connectToAddress(address)
}
fun disconnectAddress(address: String) { connectionTracker.disconnectDevice(address) }
// Optionally disconnect all connections (server and client)
fun disconnectAll() {
connectionScope.launch {
// Stop and restart to force disconnects
clientManager.stop()
serverManager.stop()
delay(200)
if (isActive && isBleTransportEnabled()) {
// Restart managers if service is active
if (isGattServerEnabled()) serverManager.start()
if (isGattClientEnabled()) clientManager.start()
}
}
}
/**
* Get connected device count
*/
fun getConnectedDeviceCount(): Int = connectionTracker.getConnectedDeviceCount()
/**
* Get debug information including power management
*/
fun getDebugInfo(): String {
return buildString {
appendLine("=== Bluetooth Connection Manager ===")
appendLine("Bluetooth MAC Address: ${bluetoothAdapter?.address}")
appendLine("Active: $isActive")
appendLine("Bluetooth Enabled: ${bluetoothAdapter?.isEnabled}")
appendLine("Has Permissions: ${permissionManager.hasBluetoothPermissions()}")
appendLine("GATT Server Active: ${serverManager.getGattServer() != null}")
appendLine()
appendLine(powerManager.getPowerInfo())
appendLine()
appendLine(connectionTracker.getDebugInfo())
}
}
// MARK: - PowerManagerDelegate Implementation
override fun onPowerModeChanged(newMode: PowerManager.PowerMode) {
Log.i(TAG, "Power mode changed to: $newMode")
connectionScope.launch {
if (!isActive || !isBleTransportEnabled()) {
serverManager.stop()
clientManager.stop()
return@launch
}
// Avoid rapid scan restarts by checking if we need to change scan behavior
val wasUsingDutyCycle = powerManager.shouldUseDutyCycle()
// Update advertising with new power settings if server enabled
val serverEnabled = isGattServerEnabled()
if (serverEnabled) {
serverManager.restartAdvertising()
} else {
serverManager.stop()
}
// Only restart scanning if the duty cycle behavior changed
val nowUsingDutyCycle = powerManager.shouldUseDutyCycle()
if (wasUsingDutyCycle != nowUsingDutyCycle) {
val clientEnabled = isGattClientEnabled()
if (clientEnabled) {
clientManager.restartScanning()
} else {
clientManager.stop()
}
}
// Enforce connection limits
enforceStrictLimits()
}
}
override fun onScanStateChanged(shouldScan: Boolean) {
if (!isActive || !isBleTransportEnabled()) {
clientManager.onScanStateChanged(false)
return
}
clientManager.onScanStateChanged(shouldScan)
}
// MARK: - Private Implementation - All moved to component managers
}
/**
* Delegate interface for Bluetooth connection manager callbacks
*/
interface BluetoothConnectionManagerDelegate {
fun onPacketReceived(
packet: BitchatPacket,
peerID: String,
device: BluetoothDevice?,
ingressLinkID: String
)
fun onDeviceConnected(device: BluetoothDevice)
fun onDeviceDisconnected(device: BluetoothDevice, linkID: String?)
fun onRSSIUpdated(deviceAddress: String, rssi: Int)
}