mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-24 23:25:19 +00:00
420 lines
16 KiB
Kotlin
420 lines
16 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
|
|
|
|
/**
|
|
* 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)
|
|
|
|
// Delegate for component managers to call back to main manager
|
|
private val componentDelegate = object : BluetoothConnectionManagerDelegate {
|
|
override fun onPacketReceived(packet: BitchatPacket, peerID: String, device: BluetoothDevice?) {
|
|
Log.d(TAG, "onPacketReceived: Packet received from ${device?.address} ($peerID)")
|
|
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)
|
|
}
|
|
|
|
override fun onDeviceConnected(device: BluetoothDevice) {
|
|
delegate?.onDeviceConnected(device)
|
|
}
|
|
|
|
override fun onDeviceDisconnected(device: BluetoothDevice) {
|
|
delegate?.onDeviceDisconnected(device)
|
|
}
|
|
|
|
override fun onRSSIUpdated(deviceAddress: String, rssi: Int) {
|
|
delegate?.onRSSIUpdated(deviceAddress, rssi)
|
|
}
|
|
}
|
|
|
|
private val serverManager = BluetoothGattServerManager(
|
|
context, connectionScope, connectionTracker, permissionManager, powerManager, componentDelegate
|
|
)
|
|
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
|
|
|
|
// Expose first-announce helpers to higher layers
|
|
fun noteAnnounceReceived(address: String) { connectionTracker.noteAnnounceReceived(address) }
|
|
fun hasSeenFirstAnnounce(address: String): Boolean = connectionTracker.hasSeenFirstAnnounce(address)
|
|
|
|
init {
|
|
powerManager.delegate = this
|
|
// Observe debug settings to enforce role state while active
|
|
try {
|
|
val dbg = com.bitchat.android.ui.debug.DebugSettingsManager.getInstance()
|
|
// Role enable/disable
|
|
connectionScope.launch {
|
|
dbg.gattServerEnabled.collect { enabled ->
|
|
if (!isActive) return@collect
|
|
if (enabled) startServer() else stopServer()
|
|
}
|
|
}
|
|
connectionScope.launch {
|
|
dbg.gattClientEnabled.collect { enabled ->
|
|
if (!isActive) return@collect
|
|
if (enabled) startClient() else stopClient()
|
|
}
|
|
}
|
|
// Connection caps: enforce on change
|
|
connectionScope.launch {
|
|
dbg.maxConnectionsOverall.collect {
|
|
if (!isActive) return@collect
|
|
connectionTracker.enforceConnectionLimits()
|
|
// Also enforce server side best-effort
|
|
serverManager.enforceServerLimit(dbg.maxServerConnections.value)
|
|
}
|
|
}
|
|
connectionScope.launch {
|
|
dbg.maxClientConnections.collect {
|
|
if (!isActive) return@collect
|
|
connectionTracker.enforceConnectionLimits()
|
|
}
|
|
}
|
|
connectionScope.launch {
|
|
dbg.maxServerConnections.collect {
|
|
if (!isActive) return@collect
|
|
serverManager.enforceServerLimit(dbg.maxServerConnections.value)
|
|
}
|
|
}
|
|
} catch (_: Exception) { }
|
|
}
|
|
|
|
/**
|
|
* Start all Bluetooth services with power optimization
|
|
*/
|
|
fun startServices(): Boolean {
|
|
Log.i(TAG, "Starting power-optimized Bluetooth services...")
|
|
|
|
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 dbg = try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance() } catch (_: Exception) { null }
|
|
val startServer = dbg?.gattServerEnabled?.value != false
|
|
val startClient = dbg?.gattClientEnabled?.value != false
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stop all Bluetooth services with proper cleanup
|
|
*/
|
|
fun stopServices() {
|
|
Log.i(TAG, "Stopping power-optimized Bluetooth services")
|
|
|
|
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")
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set app background state for power optimization
|
|
*/
|
|
fun setAppBackgroundState(inBackground: Boolean) {
|
|
powerManager.setAppBackgroundState(inBackground)
|
|
}
|
|
|
|
/**
|
|
* Broadcast packet to connected devices with connection limit enforcement
|
|
* Automatically fragments large packets to fit within BLE MTU limits
|
|
*/
|
|
fun broadcastPacket(routed: RoutedPacket) {
|
|
if (!isActive) return
|
|
|
|
packetBroadcaster.broadcastPacket(
|
|
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) return false
|
|
return packetBroadcaster.sendPacketToPeer(
|
|
RoutedPacket(packet),
|
|
peerID,
|
|
serverManager.getGattServer(),
|
|
serverManager.getCharacteristic()
|
|
)
|
|
}
|
|
|
|
|
|
// Expose role controls for debug UI
|
|
fun startServer() { connectionScope.launch { serverManager.start() } }
|
|
fun stopServer() { connectionScope.launch { serverManager.stop() } }
|
|
fun startClient() { connectionScope.launch { 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 = clientManager.connectToAddress(address)
|
|
fun disconnectAddress(address: String) {
|
|
connectionScope.launch {
|
|
try {
|
|
val dc = connectionTracker.getDeviceConnection(address)
|
|
if (dc != null) {
|
|
if (dc.gatt != null) {
|
|
try { dc.gatt.disconnect() } catch (_: Exception) { }
|
|
} else {
|
|
// Try canceling server-side connection if present
|
|
try { serverManager.getGattServer()?.cancelConnection(dc.device) } catch (_: Exception) { }
|
|
}
|
|
}
|
|
} catch (_: Exception) { }
|
|
// Cleanup tracking regardless
|
|
connectionTracker.cleanupDeviceConnection(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) {
|
|
// Restart managers if service is active
|
|
serverManager.start()
|
|
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 {
|
|
// 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 = try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().gattServerEnabled.value } catch (_: Exception) { true }
|
|
if (serverEnabled) {
|
|
serverManager.restartAdvertising()
|
|
} else {
|
|
serverManager.stop()
|
|
}
|
|
|
|
// Only restart scanning if the duty cycle behavior changed
|
|
val nowUsingDutyCycle = powerManager.shouldUseDutyCycle()
|
|
if (wasUsingDutyCycle != nowUsingDutyCycle) {
|
|
Log.d(TAG, "Duty cycle behavior changed (${wasUsingDutyCycle} -> ${nowUsingDutyCycle}), restarting scan")
|
|
val clientEnabled = try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().gattClientEnabled.value } catch (_: Exception) { true }
|
|
if (clientEnabled) {
|
|
clientManager.restartScanning()
|
|
} else {
|
|
clientManager.stop()
|
|
}
|
|
} else {
|
|
Log.d(TAG, "Duty cycle behavior unchanged, keeping existing scan state")
|
|
}
|
|
|
|
// Enforce connection limits
|
|
connectionTracker.enforceConnectionLimits()
|
|
// Best-effort server cap
|
|
try {
|
|
val maxServer = com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().maxServerConnections.value
|
|
serverManager.enforceServerLimit(maxServer)
|
|
} catch (_: Exception) { }
|
|
}
|
|
}
|
|
|
|
override fun onScanStateChanged(shouldScan: Boolean) {
|
|
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?)
|
|
fun onDeviceConnected(device: BluetoothDevice)
|
|
fun onDeviceDisconnected(device: BluetoothDevice)
|
|
fun onRSSIUpdated(deviceAddress: String, rssi: Int)
|
|
}
|