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 f9414dac..83bb2e8b 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt @@ -1,24 +1,16 @@ package com.bitchat.android.mesh -import android.Manifest import android.bluetooth.* -import android.bluetooth.le.* import android.content.Context -import android.content.pm.PackageManager -import android.os.ParcelUuid import android.util.Log -import androidx.core.app.ActivityCompat -import com.bitchat.android.protocol.BitchatPacket -import com.bitchat.android.protocol.SpecialRecipients import com.bitchat.android.model.RoutedPacket +import com.bitchat.android.protocol.BitchatPacket import kotlinx.coroutines.* -import java.util.* -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.CopyOnWriteArrayList /** * 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, @@ -28,82 +20,50 @@ class BluetoothConnectionManager( companion object { private const val TAG = "BluetoothConnectionManager" - // Use exact same UUIDs as iOS version - private val SERVICE_UUID = UUID.fromString("F47B5E2D-4A9E-4C5A-9B3F-8E1D2C3A4B5C") - private val CHARACTERISTIC_UUID = UUID.fromString("A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5D") - - // Connection management constants - private const val CONNECTION_RETRY_DELAY = 5000L - private const val MAX_CONNECTION_ATTEMPTS = 3 - private const val CLEANUP_DELAY = 500L - private const val CLEANUP_INTERVAL = 30000L // 30 seconds } // Core Bluetooth components private val bluetoothManager: BluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager private val bluetoothAdapter: BluetoothAdapter? = bluetoothManager.adapter - private val bleScanner: BluetoothLeScanner? = bluetoothAdapter?.bluetoothLeScanner - private val bleAdvertiser: BluetoothLeAdvertiser? = bluetoothAdapter?.bluetoothLeAdvertiser // Power management private val powerManager = PowerManager(context) - // GATT server for peripheral mode - private var gattServer: BluetoothGattServer? = null - private var characteristic: BluetoothGattCharacteristic? = null + // Coroutines + private val connectionScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - // Simplified connection tracking - reduced memory footprint - private val connectedDevices = ConcurrentHashMap() - private val subscribedDevices = CopyOnWriteArrayList() - public val addressPeerMap = ConcurrentHashMap() + // Component managers + private val permissionManager = BluetoothPermissionManager(context) + private val connectionTracker = BluetoothConnectionTracker(connectionScope, powerManager) + private val packetBroadcaster = BluetoothPacketBroadcaster(connectionScope, connectionTracker) - // Connection attempt tracking with automatic cleanup - private val pendingConnections = ConcurrentHashMap() + // Delegate for component managers to call back to main manager + private val componentDelegate = object : BluetoothConnectionManagerDelegate { + override fun onPacketReceived(packet: BitchatPacket, peerID: String, device: BluetoothDevice?) { + delegate?.onPacketReceived(packet, peerID, device) + } + + override fun onDeviceConnected(device: BluetoothDevice) { + delegate?.onDeviceConnected(device) + } + } + + 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 - private var scanCallback: ScanCallback? = null - private var advertiseCallback: AdvertiseCallback? = null - - // CRITICAL FIX: Scan rate limiting to prevent "scanning too frequently" errors - private var lastScanStartTime = 0L - private var lastScanStopTime = 0L - private var isCurrentlyScanning = false - private val scanRateLimit = 5000L // Minimum 5 seconds between scan start attempts // Delegate for callbacks var delegate: BluetoothConnectionManagerDelegate? = null - // Coroutines - private val connectionScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - - /** - * Consolidated device connection information - */ - private data class DeviceConnection( - val device: BluetoothDevice, - val gatt: BluetoothGatt? = null, - val characteristic: BluetoothGattCharacteristic? = null, - val rssi: Int = Int.MIN_VALUE, - val isClient: Boolean = false, - val connectedAt: Long = System.currentTimeMillis() - ) - - /** - * Connection attempt tracking with automatic expiry - */ - private data class ConnectionAttempt( - val attempts: Int, - val lastAttempt: Long = System.currentTimeMillis() - ) { - fun isExpired(): Boolean = - System.currentTimeMillis() - lastAttempt > CONNECTION_RETRY_DELAY * 2 - - fun shouldRetry(): Boolean = - attempts < MAX_CONNECTION_ATTEMPTS && - System.currentTimeMillis() - lastAttempt > CONNECTION_RETRY_DELAY - } + // Public property for address-peer mapping + val addressPeerMap get() = connectionTracker.addressPeerMap init { powerManager.delegate = this @@ -115,7 +75,7 @@ class BluetoothConnectionManager( fun startServices(): Boolean { Log.i(TAG, "Starting power-optimized Bluetooth services...") - if (!hasBluetoothPermissions()) { + if (!permissionManager.hasBluetoothPermissions()) { Log.e(TAG, "Missing Bluetooth permissions") return false } @@ -125,32 +85,30 @@ class BluetoothConnectionManager( return false } - if (bleScanner == null || bleAdvertiser == null) { - Log.e(TAG, "BLE scanner or advertiser not available") - return false - } - try { isActive = true - // Setup GATT server first - setupGattServer() - - // Start power manager and services + // Start all component managers connectionScope.launch { + // Start connection tracker first + connectionTracker.start() + + // Start power manager powerManager.start() - delay(300) // Brief delay to ensure GATT server is ready - startAdvertising() - delay(100) - - if (powerManager.shouldUseDutyCycle()) { - Log.i(TAG, "Using power-aware duty cycling") - } else { - startScanning() + // Start server manager + if (!serverManager.start()) { + Log.e(TAG, "Failed to start server manager") + this@BluetoothConnectionManager.isActive = false + return@launch } - startPeriodicCleanup() + // Start client manager + if (!clientManager.start()) { + Log.e(TAG, "Failed to start client manager") + this@BluetoothConnectionManager.isActive = false + return@launch + } Log.i(TAG, "Bluetooth services started successfully") } @@ -173,23 +131,17 @@ class BluetoothConnectionManager( isActive = false connectionScope.launch { - // Stop power manager first + // Stop component managers + clientManager.stop() + serverManager.stop() + + // Stop power manager powerManager.stop() - // Stop scanning and advertising - stopScanning() - stopAdvertising() - - // Cleanup all GATT connections with delay - cleanupAllConnections() - - // Close GATT server - gattServer?.close() - gattServer = null - - // Clear tracking - clearAllConnections() + // Stop connection tracker + connectionTracker.stop() + // Cancel the coroutine scope connectionScope.cancel() Log.i(TAG, "All Bluetooth services stopped") @@ -202,141 +154,25 @@ class BluetoothConnectionManager( fun setAppBackgroundState(inBackground: Boolean) { powerManager.setAppBackgroundState(inBackground) } - - // Function to send data to a single device (server side) - private fun notifyDevice(device: BluetoothDevice, data: ByteArray): Boolean { - return try { - characteristic?.let { char -> - char.value = data - val result = gattServer?.notifyCharacteristicChanged(device, char, false) ?: false - result - } ?: false - } catch (e: Exception) { - Log.w(TAG, "Error sending to server connection ${device.address}: ${e.message}") - connectionScope.launch { - delay(CLEANUP_DELAY) - subscribedDevices.remove(device) - addressPeerMap.remove(device.address) - } - false - } - } - - // Function to send data to a single device (client side) - private fun writeToDeviceConn(deviceConn: DeviceConnection, data: ByteArray): Boolean { - return try { - deviceConn.characteristic?.let { char -> - char.value = data - val result = deviceConn.gatt?.writeCharacteristic(char) ?: false - result - } ?: false - } catch (e: Exception) { - Log.w(TAG, "Error sending to client connection ${deviceConn.device.address}: ${e.message}") - connectionScope.launch { - delay(CLEANUP_DELAY) - cleanupDeviceConnection(deviceConn.device.address) - } - false - } - } /** * Broadcast packet to connected devices with connection limit enforcement * Automatically fragments large packets to fit within BLE MTU limits */ fun broadcastPacket(routed: RoutedPacket) { - val packet = routed.packet - if (!isActive) return - // Check if we need to fragment - if (fragmentManager != null) { - val fragments = fragmentManager.createFragments(packet) - if (fragments.size > 1) { - Log.d(TAG, "Fragmenting packet into ${fragments.size} fragments") - connectionScope.launch { - fragments.forEach { fragment -> - sendSinglePacket(fragment) - // 20ms delay between fragments (matching iOS/Rust) - delay(20) - } - } - return - } - } - - // Send single packet if no fragmentation needed - sendSinglePacket(packet) - } - - /** - * Send a single packet (fragment or whole) to all connected devices - */ - private fun sendSinglePacket(packet: BitchatPacket) { - val data = packet.toBinaryData() ?: return - Log.d(TAG, "Sending packet type ${packet.type} (${data.size} bytes) to ${subscribedDevices.size} server + ${connectedDevices.size} client connections") - - if (packet.recipientID != SpecialRecipients.BROADCAST) { - val recipientID = packet.recipientID?.let { - String(it).replace("\u0000", "").trim() - } ?: "" - - // Try to find the recipient in server connections (subscribedDevices) - val targetDevice = subscribedDevices.firstOrNull { addressPeerMap[it.address] == recipientID } - // If found, send directly - if (targetDevice != null) { - Log.d(TAG, "Send packet type ${packet.type} directly to target device for recipient $recipientID: ${targetDevice.address}") - if (notifyDevice(targetDevice, data)) - return // Sent, no need to continue - } - - // Try to find the recipient in client connections (connectedDevices) - val targetDeviceConn = connectedDevices.values.firstOrNull { addressPeerMap[it.device.address] == recipientID } - // If found, send directly - if (targetDeviceConn != null) { - Log.d(TAG, "Send packet type ${packet.type} directly to target client connection for recipient $recipientID: ${targetDeviceConn.device.address}") - if (writeToDeviceConn(targetDeviceConn, data)) - return // Sent, no need to continue - } - } - - // Else, continue with broadcasting to all devices - Log.d(TAG, "Broadcasting packet type ${packet.type} to ${subscribedDevices.size} server + ${connectedDevices.size} client connections") - - val senderID = String(packet.senderID).replace("\u0000", "") - // Send to server connections (devices connected to our GATT server) - subscribedDevices.forEach { device -> - if (device.address == routed.relayAddress) { - Log.d(TAG, "Skipping broadcast back to relayer: ${device.address}") - return@forEach - } - if (addressPeerMap[device.address] == senderID) { - Log.d(TAG, "Skipping broadcast back to sender: ${device.address}") - return@forEach - } - notifyDevice(device, data) - } - - // Send to client connections - connectedDevices.values.forEach { deviceConn -> - if (deviceConn.isClient && deviceConn.gatt != null && deviceConn.characteristic != null) { - if (deviceConn.device.address == routed.relayAddress) { - Log.d(TAG, "Skipping broadcast back to relayer: ${deviceConn.device.address}") - return@forEach - } - if (addressPeerMap[deviceConn.device.address] == senderID) { - Log.d(TAG, "Skipping broadcast back to sender: ${deviceConn.device.address}") - return@forEach - } - writeToDeviceConn(deviceConn, data) - } - } + packetBroadcaster.broadcastPacket( + routed, + serverManager.getGattServer(), + serverManager.getCharacteristic() + ) } /** * Get connected device count */ - fun getConnectedDeviceCount(): Int = connectedDevices.size + fun getConnectedDeviceCount(): Int = connectionTracker.getConnectedDeviceCount() /** * Get debug information including power management @@ -347,25 +183,12 @@ class BluetoothConnectionManager( appendLine("Bluetooth MAC Address: ${bluetoothAdapter?.address}") appendLine("Active: $isActive") appendLine("Bluetooth Enabled: ${bluetoothAdapter?.isEnabled}") - appendLine("Has Permissions: ${hasBluetoothPermissions()}") - appendLine("GATT Server Active: ${gattServer != null}") + appendLine("Has Permissions: ${permissionManager.hasBluetoothPermissions()}") + appendLine("GATT Server Active: ${serverManager.getGattServer() != null}") appendLine() appendLine(powerManager.getPowerInfo()) appendLine() - appendLine("Connected Devices: ${connectedDevices.size} / ${powerManager.getMaxConnections()}") - connectedDevices.forEach { (address, deviceConn) -> - val age = (System.currentTimeMillis() - deviceConn.connectedAt) / 1000 - appendLine(" - $address (${if (deviceConn.isClient) "client" else "server"}, ${age}s, RSSI: ${deviceConn.rssi})") - } - appendLine() - appendLine("Subscribed Devices (server mode): ${subscribedDevices.size}") - appendLine() - appendLine("Pending Connections: ${pendingConnections.size}") - val now = System.currentTimeMillis() - pendingConnections.forEach { (address, attempt) -> - val elapsed = (now - attempt.lastAttempt) / 1000 - appendLine(" - $address: ${attempt.attempts} attempts, last ${elapsed}s ago") - } + appendLine(connectionTracker.getDebugInfo()) } } @@ -379,655 +202,27 @@ class BluetoothConnectionManager( val wasUsingDutyCycle = powerManager.shouldUseDutyCycle() // Update advertising with new power settings - stopAdvertising() - delay(100) - startAdvertising() + serverManager.restartAdvertising() // 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") - stopScanning() - delay(1000) // Extra delay to avoid rate limiting - - if (nowUsingDutyCycle) { - Log.i(TAG, "Switching to duty cycle scanning mode") - // Duty cycle will handle scanning - } else { - Log.i(TAG, "Switching to continuous scanning mode") - startScanning() - } + clientManager.restartScanning() } else { Log.d(TAG, "Duty cycle behavior unchanged, keeping existing scan state") } // Enforce connection limits - enforceConnectionLimits() + connectionTracker.enforceConnectionLimits() } } override fun onScanStateChanged(shouldScan: Boolean) { - if (shouldScan) { - startScanning() - } else { - stopScanning() - } + clientManager.onScanStateChanged(shouldScan) } - // MARK: - Private Implementation - - private fun hasBluetoothPermissions(): Boolean { - val permissions = mutableListOf() - - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { - permissions.addAll(listOf( - Manifest.permission.BLUETOOTH_ADVERTISE, - Manifest.permission.BLUETOOTH_CONNECT, - Manifest.permission.BLUETOOTH_SCAN - )) - } else { - permissions.addAll(listOf( - Manifest.permission.BLUETOOTH, - Manifest.permission.BLUETOOTH_ADMIN - )) - } - - permissions.addAll(listOf( - Manifest.permission.ACCESS_COARSE_LOCATION, - Manifest.permission.ACCESS_FINE_LOCATION - )) - - return permissions.all { - ActivityCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED - } - } - - @Suppress("DEPRECATION") - private fun setupGattServer() { - if (!hasBluetoothPermissions()) return - - val serverCallback = object : BluetoothGattServerCallback() { - override fun onConnectionStateChange(device: BluetoothDevice, status: Int, newState: Int) { - // Guard against callbacks after service shutdown - if (!isActive) { - Log.d(TAG, "Server: Ignoring connection state change after shutdown") - return - } - - when (newState) { - BluetoothProfile.STATE_CONNECTED -> { - Log.d(TAG, "Server: Device connected ${device.address}") - val deviceConn = DeviceConnection( - device = device, - isClient = false - ) - connectedDevices[device.address] = deviceConn - } - BluetoothProfile.STATE_DISCONNECTED -> { - Log.d(TAG, "Server: Device disconnected ${device.address}") - cleanupDeviceConnection(device.address) - } - } - } - - override fun onServiceAdded(status: Int, service: BluetoothGattService) { - // Guard against callbacks after service shutdown - if (!isActive) { - Log.d(TAG, "Server: Ignoring service added callback after shutdown") - return - } - - if (status == BluetoothGatt.GATT_SUCCESS) { - Log.d(TAG, "Server: Service added successfully: ${service.uuid}") - } else { - Log.e(TAG, "Server: Failed to add service: ${service.uuid}, status: $status") - } - } - - override fun onCharacteristicWriteRequest( - device: BluetoothDevice, - requestId: Int, - characteristic: BluetoothGattCharacteristic, - preparedWrite: Boolean, - responseNeeded: Boolean, - offset: Int, - value: ByteArray - ) { - // Guard against callbacks after service shutdown - if (!isActive) { - Log.d(TAG, "Server: Ignoring characteristic write after shutdown") - return - } - - if (characteristic.uuid == CHARACTERISTIC_UUID) { - Log.d(TAG, "Server: Received packet from ${device.address}, size: ${value.size} bytes") - val packet = BitchatPacket.fromBinaryData(value) - if (packet != null) { - val peerID = String(packet.senderID).replace("\u0000", "") - Log.d(TAG, "Server: Parsed packet type ${packet.type} from $peerID") - delegate?.onPacketReceived(packet, peerID, device) - } else { - Log.w(TAG, "Server: Failed to parse packet from ${device.address}, size: ${value.size} bytes") - Log.w(TAG, "Server: Packet data: ${value.joinToString(" ") { "%02x".format(it) }}") - } - - if (responseNeeded) { - gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, null) - } - } - } - - override fun onDescriptorWriteRequest( - device: BluetoothDevice, - requestId: Int, - descriptor: BluetoothGattDescriptor, - preparedWrite: Boolean, - responseNeeded: Boolean, - offset: Int, - value: ByteArray - ) { - // Guard against callbacks after service shutdown - if (!isActive) { - Log.d(TAG, "Server: Ignoring descriptor write after shutdown") - return - } - - if (BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE.contentEquals(value)) { - Log.d(TAG, "Device ${device.address} subscribed to notifications") - subscribedDevices.add(device) - - connectionScope.launch { - delay(100) - if (isActive) { // Check if still active - delegate?.onDeviceConnected(device) - } - } - } - - if (responseNeeded) { - gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, null) - } - } - } - - // Proper cleanup sequencing to prevent race conditions - gattServer?.let { server -> - Log.d(TAG, "Cleaning up existing GATT server") - try { - server.close() - } catch (e: Exception) { - Log.w(TAG, "Error closing existing GATT server: ${e.message}") - } - } - - // Small delay to ensure cleanup is complete - Thread.sleep(100) - - if (!isActive) { - Log.d(TAG, "Service inactive, skipping GATT server creation") - return - } - - // Create new server - gattServer = bluetoothManager.openGattServer(context, serverCallback) - - // Create characteristic with notification support - characteristic = BluetoothGattCharacteristic( - CHARACTERISTIC_UUID, - BluetoothGattCharacteristic.PROPERTY_READ or - BluetoothGattCharacteristic.PROPERTY_WRITE or - BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE or - BluetoothGattCharacteristic.PROPERTY_NOTIFY, - BluetoothGattCharacteristic.PERMISSION_READ or - BluetoothGattCharacteristic.PERMISSION_WRITE - ) - - val descriptor = BluetoothGattDescriptor( - UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"), - BluetoothGattDescriptor.PERMISSION_READ or BluetoothGattDescriptor.PERMISSION_WRITE - ) - characteristic?.addDescriptor(descriptor) - - val service = BluetoothGattService(SERVICE_UUID, BluetoothGattService.SERVICE_TYPE_PRIMARY) - service.addCharacteristic(characteristic) - - gattServer?.addService(service) - - Log.i(TAG, "GATT server setup complete") - } - - @Suppress("DEPRECATION") - private fun startAdvertising() { - if (!hasBluetoothPermissions() || bleAdvertiser == null || !isActive) return - - val settings = powerManager.getAdvertiseSettings() - - val data = AdvertiseData.Builder() - .addServiceUuid(ParcelUuid(SERVICE_UUID)) - .setIncludeTxPowerLevel(false) - .setIncludeDeviceName(false) - .build() - - advertiseCallback = object : AdvertiseCallback() { - override fun onStartSuccess(settingsInEffect: AdvertiseSettings) { - Log.i(TAG, "Advertising started (power mode: ${powerManager.getPowerInfo().split("Current Mode: ")[1].split("\n")[0]})") - } - - override fun onStartFailure(errorCode: Int) { - Log.e(TAG, "Advertising failed: $errorCode") - } - } - - try { - bleAdvertiser.startAdvertising(settings, data, advertiseCallback) - } catch (e: Exception) { - Log.e(TAG, "Exception starting advertising: ${e.message}") - } - } - - @Suppress("DEPRECATION") - private fun stopAdvertising() { - if (!hasBluetoothPermissions() || bleAdvertiser == null) return - try { - advertiseCallback?.let { bleAdvertiser.stopAdvertising(it) } - } catch (e: Exception) { - Log.w(TAG, "Error stopping advertising: ${e.message}") - } - } - - @Suppress("DEPRECATION") - private fun startScanning() { - if (!hasBluetoothPermissions() || bleScanner == null || !isActive) return - - // CRITICAL FIX: Rate limit scan starts to prevent "scanning too frequently" errors - val currentTime = System.currentTimeMillis() - if (isCurrentlyScanning) { - Log.d(TAG, "Scan already in progress, skipping start request") - return - } - - val timeSinceLastStart = currentTime - lastScanStartTime - if (timeSinceLastStart < scanRateLimit) { - val remainingWait = scanRateLimit - timeSinceLastStart - Log.w(TAG, "Scan rate limited: need to wait ${remainingWait}ms before starting scan") - - // Schedule delayed scan start - connectionScope.launch { - delay(remainingWait) - if (isActive && !isCurrentlyScanning) { - startScanning() - } - } - return - } - - // DIAGNOSTIC: Add both filtered and unfiltered scanning for debugging - val scanFilter = ScanFilter.Builder() - .setServiceUuid(ParcelUuid(SERVICE_UUID)) - .build() - - - val scanFilters = listOf(scanFilter) - - Log.d(TAG, "Starting BLE scan with target service UUID: $SERVICE_UUID") - - scanCallback = object : ScanCallback() { - override fun onScanResult(callbackType: Int, result: ScanResult) { - // DEBUG: Log ALL scan results first - val device = result.device - val rssi = result.rssi - // Log.d(TAG, "Scan result: device: ${device.address}, Name: '${device.name}', RSSI: $rssi") - handleScanResult(result) - } - - override fun onBatchScanResults(results: MutableList) { - Log.d(TAG, "Batch scan results received: ${results.size} devices") - results.forEach { result -> - handleScanResult(result) - } - } - - override fun onScanFailed(errorCode: Int) { - Log.e(TAG, "Scan failed: $errorCode") - isCurrentlyScanning = false - lastScanStopTime = System.currentTimeMillis() - - when (errorCode) { - 1 -> Log.e(TAG, "SCAN_FAILED_ALREADY_STARTED") - 2 -> Log.e(TAG, "SCAN_FAILED_APPLICATION_REGISTRATION_FAILED") - 3 -> Log.e(TAG, "SCAN_FAILED_INTERNAL_ERROR") - 4 -> Log.e(TAG, "SCAN_FAILED_FEATURE_UNSUPPORTED") - 5 -> Log.e(TAG, "SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES") - 6 -> { - Log.e(TAG, "SCAN_FAILED_SCANNING_TOO_FREQUENTLY") - Log.w(TAG, "Scan failed due to rate limiting - will retry after delay") - connectionScope.launch { - delay(10000) // Wait 10 seconds before retrying - if (isActive) { - startScanning() - } - } - } - else -> Log.e(TAG, "Unknown scan failure code: $errorCode") - } - } - } - - try { - lastScanStartTime = currentTime - isCurrentlyScanning = true - - bleScanner.startScan(scanFilters, powerManager.getScanSettings(), scanCallback) - Log.d(TAG, "BLE scan started successfully") - } catch (e: Exception) { - Log.e(TAG, "Exception starting scan: ${e.message}") - isCurrentlyScanning = false - } - } - - @Suppress("DEPRECATION") - private fun stopScanning() { - if (!hasBluetoothPermissions() || bleScanner == null) return - - if (isCurrentlyScanning) { - try { - scanCallback?.let { - bleScanner.stopScan(it) - Log.d(TAG, "BLE scan stopped successfully") - } - } catch (e: Exception) { - Log.w(TAG, "Error stopping scan: ${e.message}") - } - - isCurrentlyScanning = false - lastScanStopTime = System.currentTimeMillis() - } - } - - private fun handleScanResult(result: ScanResult) { - val device = result.device - 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 - } - - // FIXED: Extract peer ID from device name for iOS compatibility - val deviceName = device.name - val extractedPeerID = if (deviceName != null && deviceName.length == 8) { - deviceName - } else { - null - } - - // Power-aware RSSI filtering - if (rssi < powerManager.getRSSIThreshold()) { - Log.d(TAG, "Skipping device $deviceAddress due to weak signal: $rssi < ${powerManager.getRSSIThreshold()}") - return - } - - // CRITICAL FIX: Prevent multiple simultaneous connections to same device - // Check if already connected OR already attempting to connect - if (connectedDevices.containsKey(deviceAddress)) { - // Log.d(TAG, "Device $deviceAddress already connected, skipping") - return - } - - // CRITICAL FIX: Check if connection attempt is already in progress - val existingAttempt = pendingConnections[deviceAddress] - if (existingAttempt != null && !existingAttempt.isExpired()) { - if (!existingAttempt.shouldRetry()) { - Log.d(TAG, "Connection to $deviceAddress already in progress or too many recent attempts (${existingAttempt.attempts})") - return - } - } - - if (connectedDevices.size >= powerManager.getMaxConnections()) { - Log.d(TAG, "Connection limit reached (${powerManager.getMaxConnections()})") - return - } - - // CRITICAL FIX: Use synchronized block to prevent race conditions - synchronized(pendingConnections) { - // Double-check inside synchronized block - val currentAttempt = pendingConnections[deviceAddress] - if (currentAttempt != null && !currentAttempt.isExpired() && !currentAttempt.shouldRetry()) { - Log.d(TAG, "Connection to $deviceAddress blocked by concurrent attempt check") - return - } - - // Update connection attempt atomically - val attempts = (currentAttempt?.attempts ?: 0) + 1 - pendingConnections[deviceAddress] = ConnectionAttempt(attempts) - - // Start connection immediately while holding lock - connectToDevice(device, rssi) - } - } - - @Suppress("DEPRECATION") - private fun connectToDevice(device: BluetoothDevice, rssi: Int) { - if (!hasBluetoothPermissions()) return - - val deviceAddress = device.address - Log.d(TAG, "Connecting to bitchat device: $deviceAddress") - - val gattCallback = object : BluetoothGattCallback() { - override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { - Log.d(TAG, "Client: Connection state change - Device: $deviceAddress, Status: $status, NewState: $newState") - - if (newState == BluetoothProfile.STATE_CONNECTED && status == BluetoothGatt.GATT_SUCCESS) { - Log.i(TAG, "Client: Successfully connected to $deviceAddress. Requesting MTU...") - // FIX: Request a larger MTU. Must be done before any data transfer. - // 517 is the maximum supported MTU size on Android. - connectionScope.launch { - delay(200) // A small delay can improve reliability of MTU request. - gatt.requestMtu(517) - } - } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { - if (status != BluetoothGatt.GATT_SUCCESS) { - Log.w(TAG, "Client: Disconnected from $deviceAddress with error status $status") - if (status == 147) { - Log.e(TAG, "Client: Connection establishment failed (status 147) for $deviceAddress") - } - } else { - Log.d(TAG, "Client: Cleanly disconnected from $deviceAddress") - } - - cleanupDeviceConnection(deviceAddress) - - connectionScope.launch { - delay(CLEANUP_DELAY) - try { - gatt.close() - } catch (e: Exception) { - Log.w(TAG, "Error closing GATT: ${e.message}") - } - } - } - } - - override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) { - val deviceAddress = gatt.device.address - Log.i(TAG, "Client: MTU changed for $deviceAddress to $mtu with status $status") - - if (status == BluetoothGatt.GATT_SUCCESS) { - Log.i(TAG, "MTU successfully negotiated for $deviceAddress. Discovering services.") - - // Now that MTU is set, connection is fully ready. - val deviceConn = DeviceConnection( - device = gatt.device, - gatt = gatt, - rssi = rssi, - isClient = true - ) - connectedDevices[deviceAddress] = deviceConn - pendingConnections.remove(deviceAddress) - - // Start service discovery only AFTER MTU is set. - gatt.discoverServices() - } else { - Log.w(TAG, "MTU negotiation failed for $deviceAddress with status: $status. Disconnecting.") - pendingConnections.remove(deviceAddress) - gatt.disconnect() - } - } - - override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { - if (status == BluetoothGatt.GATT_SUCCESS) { - val service = gatt.getService(SERVICE_UUID) - if (service != null) { - val characteristic = service.getCharacteristic(CHARACTERISTIC_UUID) - if (characteristic != null) { - connectedDevices[deviceAddress]?.let { deviceConn -> - val updatedConn = deviceConn.copy(characteristic = characteristic) - connectedDevices[deviceAddress] = updatedConn - Log.d(TAG, "Client: Updated device connection with characteristic for $deviceAddress") - } - - gatt.setCharacteristicNotification(characteristic, true) - val descriptor = characteristic.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")) - if (descriptor != null) { - descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE - gatt.writeDescriptor(descriptor) - - connectionScope.launch { - delay(200) - Log.i(TAG, "Client: Connection setup complete for $deviceAddress") - delegate?.onDeviceConnected(device) - } - } else { - Log.e(TAG, "Client: CCCD descriptor not found for $deviceAddress") - gatt.disconnect() - } - } else { - Log.e(TAG, "Client: Required characteristic not found for $deviceAddress") - gatt.disconnect() - } - } else { - Log.e(TAG, "Client: Required service not found for $deviceAddress") - gatt.disconnect() - } - } else { - Log.e(TAG, "Client: Service discovery failed with status $status for $deviceAddress") - gatt.disconnect() - } - } - - override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { - val value = characteristic.value - Log.d(TAG, "Client: Received packet from ${gatt.device.address}, size: ${value.size} bytes") - val packet = BitchatPacket.fromBinaryData(value) - if (packet != null) { - val peerID = String(packet.senderID).replace("\u0000", "") - Log.d(TAG, "Client: Parsed packet type ${packet.type} from $peerID") - delegate?.onPacketReceived(packet, peerID, gatt.device) - } else { - Log.w(TAG, "Client: Failed to parse packet from ${gatt.device.address}, size: ${value.size} bytes") - Log.w(TAG, "Client: Packet data: ${value.joinToString(" ") { "%02x".format(it) }}") - } - } - } - - try { - Log.d(TAG, "Client: Attempting GATT connection to $deviceAddress with autoConnect=false") - val gatt = device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE) - if (gatt == null) { - Log.e(TAG, "connectGatt returned null for $deviceAddress") - pendingConnections.remove(deviceAddress) - } else { - Log.d(TAG, "Client: GATT connection initiated successfully for $deviceAddress") - } - } catch (e: Exception) { - Log.e(TAG, "Client: Exception connecting to $deviceAddress: ${e.message}") - pendingConnections.remove(deviceAddress) - } - } - - private fun startPeriodicCleanup() { - connectionScope.launch { - while (isActive) { - delay(CLEANUP_INTERVAL) - - if (!isActive) break - - try { - // Clean up expired pending connections - val expiredConnections = pendingConnections.filter { it.value.isExpired() } - expiredConnections.keys.forEach { pendingConnections.remove(it) } - - // Log cleanup if any - if (expiredConnections.isNotEmpty()) { - Log.d(TAG, "Cleaned up ${expiredConnections.size} expired connection attempts") - } - - // Log current state - Log.d(TAG, "Periodic cleanup: ${connectedDevices.size} connections, ${pendingConnections.size} pending") - - } catch (e: Exception) { - Log.w(TAG, "Error in periodic cleanup: ${e.message}") - } - } - } - } - - private fun enforceConnectionLimits() { - val maxConnections = powerManager.getMaxConnections() - if (connectedDevices.size > maxConnections) { - Log.i(TAG, "Enforcing connection limit: ${connectedDevices.size} > $maxConnections") - - // Disconnect oldest client connections first - val sortedConnections = connectedDevices.values - .filter { it.isClient } - .sortedBy { it.connectedAt } - - val toDisconnect = sortedConnections.take(connectedDevices.size - maxConnections) - toDisconnect.forEach { deviceConn -> - Log.d(TAG, "Disconnecting ${deviceConn.device.address} due to connection limit") - deviceConn.gatt?.disconnect() - } - } - } - - private fun cleanupDeviceConnection(deviceAddress: String) { - connectedDevices.remove(deviceAddress)?.let { deviceConn -> - subscribedDevices.removeAll { it.address == deviceAddress } - addressPeerMap.remove(deviceAddress) - } - // CRITICAL FIX: Always remove from pending connections when cleaning up - // This prevents failed connections from blocking future attempts - pendingConnections.remove(deviceAddress) - Log.d(TAG, "Cleaned up device connection for $deviceAddress") - } - - private fun cleanupAllConnections() { - connectedDevices.values.forEach { deviceConn -> - deviceConn.gatt?.disconnect() - } - - connectionScope.launch { - delay(CLEANUP_DELAY) - - connectedDevices.values.forEach { deviceConn -> - try { - deviceConn.gatt?.close() - } catch (e: Exception) { - Log.w(TAG, "Error closing GATT during cleanup: ${e.message}") - } - } - } - } - - private fun clearAllConnections() { - connectedDevices.clear() - subscribedDevices.clear() - addressPeerMap.clear() - pendingConnections.clear() - } + // MARK: - Private Implementation - All moved to component managers } /** diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionTracker.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionTracker.kt new file mode 100644 index 00000000..aa7c23c2 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionTracker.kt @@ -0,0 +1,305 @@ +package com.bitchat.android.mesh + +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothGatt +import android.bluetooth.BluetoothGattCharacteristic +import android.util.Log +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.CopyOnWriteArrayList + +/** + * Tracks all Bluetooth connections and handles cleanup + */ +class BluetoothConnectionTracker( + private val connectionScope: CoroutineScope, + private val powerManager: PowerManager +) { + + companion object { + private const val TAG = "BluetoothConnectionTracker" + private const val CONNECTION_RETRY_DELAY = 5000L + private const val MAX_CONNECTION_ATTEMPTS = 3 + private const val CLEANUP_DELAY = 500L + private const val CLEANUP_INTERVAL = 30000L // 30 seconds + } + + // Connection tracking - reduced memory footprint + private val connectedDevices = ConcurrentHashMap() + private val subscribedDevices = CopyOnWriteArrayList() + val addressPeerMap = ConcurrentHashMap() + + // Connection attempt tracking with automatic cleanup + private val pendingConnections = ConcurrentHashMap() + + // State management + private var isActive = false + + /** + * Consolidated device connection information + */ + data class DeviceConnection( + val device: BluetoothDevice, + val gatt: BluetoothGatt? = null, + val characteristic: BluetoothGattCharacteristic? = null, + val rssi: Int = Int.MIN_VALUE, + val isClient: Boolean = false, + val connectedAt: Long = System.currentTimeMillis() + ) + + /** + * Connection attempt tracking with automatic expiry + */ + data class ConnectionAttempt( + val attempts: Int, + val lastAttempt: Long = System.currentTimeMillis() + ) { + fun isExpired(): Boolean = + System.currentTimeMillis() - lastAttempt > CONNECTION_RETRY_DELAY * 2 + + fun shouldRetry(): Boolean = + attempts < MAX_CONNECTION_ATTEMPTS && + System.currentTimeMillis() - lastAttempt > CONNECTION_RETRY_DELAY + } + + /** + * Start the connection tracker + */ + fun start() { + isActive = true + startPeriodicCleanup() + } + + /** + * Stop the connection tracker + */ + fun stop() { + isActive = false + cleanupAllConnections() + clearAllConnections() + } + + /** + * Add a device connection + */ + fun addDeviceConnection(deviceAddress: String, deviceConn: DeviceConnection) { + connectedDevices[deviceAddress] = deviceConn + pendingConnections.remove(deviceAddress) + } + + /** + * Update a device connection + */ + fun updateDeviceConnection(deviceAddress: String, deviceConn: DeviceConnection) { + connectedDevices[deviceAddress] = deviceConn + } + + /** + * Get a device connection + */ + fun getDeviceConnection(deviceAddress: String): DeviceConnection? { + return connectedDevices[deviceAddress] + } + + /** + * Get all connected devices + */ + fun getConnectedDevices(): Map { + return connectedDevices.toMap() + } + + /** + * Get subscribed devices (for server connections) + */ + fun getSubscribedDevices(): List { + return subscribedDevices.toList() + } + + /** + * Add a subscribed device + */ + fun addSubscribedDevice(device: BluetoothDevice) { + subscribedDevices.add(device) + } + + /** + * Remove a subscribed device + */ + fun removeSubscribedDevice(device: BluetoothDevice) { + subscribedDevices.remove(device) + } + + /** + * Check if device is already connected + */ + fun isDeviceConnected(deviceAddress: String): Boolean { + return connectedDevices.containsKey(deviceAddress) + } + + /** + * Check if connection attempt is allowed + */ + fun isConnectionAttemptAllowed(deviceAddress: String): Boolean { + val existingAttempt = pendingConnections[deviceAddress] + return existingAttempt?.let { + it.isExpired() || it.shouldRetry() + } ?: true + } + + /** + * Add a pending connection attempt + */ + fun addPendingConnection(deviceAddress: String): Boolean { + synchronized(pendingConnections) { + // Double-check inside synchronized block + val currentAttempt = pendingConnections[deviceAddress] + if (currentAttempt != null && !currentAttempt.isExpired() && !currentAttempt.shouldRetry()) { + return false + } + + // Update connection attempt atomically + val attempts = (currentAttempt?.attempts ?: 0) + 1 + pendingConnections[deviceAddress] = ConnectionAttempt(attempts) + return true + } + } + + /** + * Remove a pending connection + */ + fun removePendingConnection(deviceAddress: String) { + pendingConnections.remove(deviceAddress) + } + + /** + * Get connected device count + */ + fun getConnectedDeviceCount(): Int = connectedDevices.size + + /** + * Check if connection limit is reached + */ + fun isConnectionLimitReached(): Boolean { + return connectedDevices.size >= powerManager.getMaxConnections() + } + + /** + * Enforce connection limits by disconnecting oldest connections + */ + fun enforceConnectionLimits() { + val maxConnections = powerManager.getMaxConnections() + if (connectedDevices.size > maxConnections) { + Log.i(TAG, "Enforcing connection limit: ${connectedDevices.size} > $maxConnections") + + // Disconnect oldest client connections first + val sortedConnections = connectedDevices.values + .filter { it.isClient } + .sortedBy { it.connectedAt } + + val toDisconnect = sortedConnections.take(connectedDevices.size - maxConnections) + toDisconnect.forEach { deviceConn -> + Log.d(TAG, "Disconnecting ${deviceConn.device.address} due to connection limit") + deviceConn.gatt?.disconnect() + } + } + } + + /** + * Clean up a specific device connection + */ + fun cleanupDeviceConnection(deviceAddress: String) { + connectedDevices.remove(deviceAddress)?.let { deviceConn -> + subscribedDevices.removeAll { it.address == deviceAddress } + addressPeerMap.remove(deviceAddress) + } + // CRITICAL FIX: Always remove from pending connections when cleaning up + // This prevents failed connections from blocking future attempts + pendingConnections.remove(deviceAddress) + Log.d(TAG, "Cleaned up device connection for $deviceAddress") + } + + /** + * Clean up all connections + */ + private fun cleanupAllConnections() { + connectedDevices.values.forEach { deviceConn -> + deviceConn.gatt?.disconnect() + } + + connectionScope.launch { + delay(CLEANUP_DELAY) + + connectedDevices.values.forEach { deviceConn -> + try { + deviceConn.gatt?.close() + } catch (e: Exception) { + Log.w(TAG, "Error closing GATT during cleanup: ${e.message}") + } + } + } + } + + /** + * Clear all connection tracking + */ + private fun clearAllConnections() { + connectedDevices.clear() + subscribedDevices.clear() + addressPeerMap.clear() + pendingConnections.clear() + } + + /** + * Start periodic cleanup of expired connections + */ + private fun startPeriodicCleanup() { + connectionScope.launch { + while (isActive) { + delay(CLEANUP_INTERVAL) + + if (!isActive) break + + try { + // Clean up expired pending connections + val expiredConnections = pendingConnections.filter { it.value.isExpired() } + expiredConnections.keys.forEach { pendingConnections.remove(it) } + + // Log cleanup if any + if (expiredConnections.isNotEmpty()) { + Log.d(TAG, "Cleaned up ${expiredConnections.size} expired connection attempts") + } + + // Log current state + Log.d(TAG, "Periodic cleanup: ${connectedDevices.size} connections, ${pendingConnections.size} pending") + + } catch (e: Exception) { + Log.w(TAG, "Error in periodic cleanup: ${e.message}") + } + } + } + } + + /** + * Get debug information + */ + fun getDebugInfo(): String { + return buildString { + appendLine("Connected Devices: ${connectedDevices.size} / ${powerManager.getMaxConnections()}") + connectedDevices.forEach { (address, deviceConn) -> + val age = (System.currentTimeMillis() - deviceConn.connectedAt) / 1000 + appendLine(" - $address (${if (deviceConn.isClient) "client" else "server"}, ${age}s, RSSI: ${deviceConn.rssi})") + } + appendLine() + appendLine("Subscribed Devices (server mode): ${subscribedDevices.size}") + appendLine() + appendLine("Pending Connections: ${pendingConnections.size}") + val now = System.currentTimeMillis() + pendingConnections.forEach { (address, attempt) -> + val elapsed = (now - attempt.lastAttempt) / 1000 + appendLine(" - $address: ${attempt.attempts} attempts, last ${elapsed}s ago") + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothGattClientManager.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothGattClientManager.kt new file mode 100644 index 00000000..3b112478 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothGattClientManager.kt @@ -0,0 +1,420 @@ +package com.bitchat.android.mesh + +import android.bluetooth.* +import android.bluetooth.le.BluetoothLeScanner +import android.bluetooth.le.ScanCallback +import android.bluetooth.le.ScanFilter +import android.bluetooth.le.ScanResult +import android.content.Context +import android.os.ParcelUuid +import android.util.Log +import com.bitchat.android.protocol.BitchatPacket +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import java.util.* + +/** + * Manages GATT client operations, scanning, and client-side connections + */ +class BluetoothGattClientManager( + private val context: Context, + private val connectionScope: CoroutineScope, + private val connectionTracker: BluetoothConnectionTracker, + private val permissionManager: BluetoothPermissionManager, + private val powerManager: PowerManager, + private val delegate: BluetoothConnectionManagerDelegate? +) { + + companion object { + private const val TAG = "BluetoothGattClientManager" + // Use exact same UUIDs as iOS version + private val SERVICE_UUID = UUID.fromString("F47B5E2D-4A9E-4C5A-9B3F-8E1D2C3A4B5C") + private val CHARACTERISTIC_UUID = UUID.fromString("A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5D") + } + + // Core Bluetooth components + private val bluetoothManager: BluetoothManager = + context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager + private val bluetoothAdapter: BluetoothAdapter? = bluetoothManager.adapter + private val bleScanner: BluetoothLeScanner? = bluetoothAdapter?.bluetoothLeScanner + + // Scan management + private var scanCallback: ScanCallback? = null + + // CRITICAL FIX: Scan rate limiting to prevent "scanning too frequently" errors + private var lastScanStartTime = 0L + private var lastScanStopTime = 0L + private var isCurrentlyScanning = false + private val scanRateLimit = 5000L // Minimum 5 seconds between scan start attempts + + // State management + private var isActive = false + + /** + * Start client manager + */ + fun start(): Boolean { + if (!permissionManager.hasBluetoothPermissions()) { + Log.e(TAG, "Missing Bluetooth permissions") + return false + } + + if (bluetoothAdapter?.isEnabled != true) { + Log.e(TAG, "Bluetooth is not enabled") + return false + } + + if (bleScanner == null) { + Log.e(TAG, "BLE scanner not available") + return false + } + + isActive = true + + connectionScope.launch { + if (powerManager.shouldUseDutyCycle()) { + Log.i(TAG, "Using power-aware duty cycling") + } else { + startScanning() + } + } + + return true + } + + /** + * Stop client manager + */ + fun stop() { + isActive = false + + connectionScope.launch { + stopScanning() + Log.i(TAG, "GATT client manager stopped") + } + } + + /** + * Handle scan state changes from power manager + */ + fun onScanStateChanged(shouldScan: Boolean) { + if (shouldScan) { + startScanning() + } else { + stopScanning() + } + } + + /** + * Start scanning with rate limiting + */ + @Suppress("DEPRECATION") + private fun startScanning() { + if (!permissionManager.hasBluetoothPermissions() || bleScanner == null || !isActive) return + + // CRITICAL FIX: Rate limit scan starts to prevent "scanning too frequently" errors + val currentTime = System.currentTimeMillis() + if (isCurrentlyScanning) { + Log.d(TAG, "Scan already in progress, skipping start request") + return + } + + val timeSinceLastStart = currentTime - lastScanStartTime + if (timeSinceLastStart < scanRateLimit) { + val remainingWait = scanRateLimit - timeSinceLastStart + Log.w(TAG, "Scan rate limited: need to wait ${remainingWait}ms before starting scan") + + // Schedule delayed scan start + connectionScope.launch { + delay(remainingWait) + if (isActive && !isCurrentlyScanning) { + startScanning() + } + } + return + } + + val scanFilter = ScanFilter.Builder() + .setServiceUuid(ParcelUuid(SERVICE_UUID)) + .build() + + val scanFilters = listOf(scanFilter) + + Log.d(TAG, "Starting BLE scan with target service UUID: $SERVICE_UUID") + + scanCallback = object : ScanCallback() { + override fun onScanResult(callbackType: Int, result: ScanResult) { + handleScanResult(result) + } + + override fun onBatchScanResults(results: MutableList) { + Log.d(TAG, "Batch scan results received: ${results.size} devices") + results.forEach { result -> + handleScanResult(result) + } + } + + override fun onScanFailed(errorCode: Int) { + Log.e(TAG, "Scan failed: $errorCode") + isCurrentlyScanning = false + lastScanStopTime = System.currentTimeMillis() + + when (errorCode) { + 1 -> Log.e(TAG, "SCAN_FAILED_ALREADY_STARTED") + 2 -> Log.e(TAG, "SCAN_FAILED_APPLICATION_REGISTRATION_FAILED") + 3 -> Log.e(TAG, "SCAN_FAILED_INTERNAL_ERROR") + 4 -> Log.e(TAG, "SCAN_FAILED_FEATURE_UNSUPPORTED") + 5 -> Log.e(TAG, "SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES") + 6 -> { + Log.e(TAG, "SCAN_FAILED_SCANNING_TOO_FREQUENTLY") + Log.w(TAG, "Scan failed due to rate limiting - will retry after delay") + connectionScope.launch { + delay(10000) // Wait 10 seconds before retrying + if (isActive) { + startScanning() + } + } + } + else -> Log.e(TAG, "Unknown scan failure code: $errorCode") + } + } + } + + try { + lastScanStartTime = currentTime + isCurrentlyScanning = true + + bleScanner.startScan(scanFilters, powerManager.getScanSettings(), scanCallback) + Log.d(TAG, "BLE scan started successfully") + } catch (e: Exception) { + Log.e(TAG, "Exception starting scan: ${e.message}") + isCurrentlyScanning = false + } + } + + /** + * Stop scanning + */ + @Suppress("DEPRECATION") + private fun stopScanning() { + if (!permissionManager.hasBluetoothPermissions() || bleScanner == null) return + + if (isCurrentlyScanning) { + try { + scanCallback?.let { + bleScanner.stopScan(it) + Log.d(TAG, "BLE scan stopped successfully") + } + } catch (e: Exception) { + Log.w(TAG, "Error stopping scan: ${e.message}") + } + + isCurrentlyScanning = false + lastScanStopTime = System.currentTimeMillis() + } + } + + /** + * Handle scan result and initiate connection if appropriate + */ + private fun handleScanResult(result: ScanResult) { + val device = result.device + 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 + } + + // Power-aware RSSI filtering + if (rssi < powerManager.getRSSIThreshold()) { + Log.d(TAG, "Skipping device $deviceAddress due to weak signal: $rssi < ${powerManager.getRSSIThreshold()}") + return + } + + // Check if already connected OR already attempting to connect + if (connectionTracker.isDeviceConnected(deviceAddress)) { + return + } + + // Check if connection attempt is allowed + if (!connectionTracker.isConnectionAttemptAllowed(deviceAddress)) { + Log.d(TAG, "Connection to $deviceAddress not allowed due to recent attempts") + return + } + + if (connectionTracker.isConnectionLimitReached()) { + Log.d(TAG, "Connection limit reached (${powerManager.getMaxConnections()})") + return + } + + // Add pending connection and start connection + if (connectionTracker.addPendingConnection(deviceAddress)) { + connectToDevice(device, rssi) + } + } + + /** + * Connect to a device as GATT client + */ + @Suppress("DEPRECATION") + private fun connectToDevice(device: BluetoothDevice, rssi: Int) { + if (!permissionManager.hasBluetoothPermissions()) return + + val deviceAddress = device.address + Log.i(TAG, "Connecting to bitchat device: $deviceAddress") + + val gattCallback = object : BluetoothGattCallback() { + override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { + Log.d(TAG, "Client: Connection state change - Device: $deviceAddress, Status: $status, NewState: $newState") + + if (newState == BluetoothProfile.STATE_CONNECTED && status == BluetoothGatt.GATT_SUCCESS) { + Log.i(TAG, "Client: Successfully connected to $deviceAddress. Requesting MTU...") + // Request a larger MTU. Must be done before any data transfer. + connectionScope.launch { + delay(200) // A small delay can improve reliability of MTU request. + gatt.requestMtu(517) + } + } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { + if (status != BluetoothGatt.GATT_SUCCESS) { + Log.w(TAG, "Client: Disconnected from $deviceAddress with error status $status") + if (status == 147) { + Log.e(TAG, "Client: Connection establishment failed (status 147) for $deviceAddress") + } + } else { + Log.d(TAG, "Client: Cleanly disconnected from $deviceAddress") + } + + connectionTracker.cleanupDeviceConnection(deviceAddress) + + connectionScope.launch { + delay(500) // CLEANUP_DELAY + try { + gatt.close() + } catch (e: Exception) { + Log.w(TAG, "Error closing GATT: ${e.message}") + } + } + } + } + + override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) { + val deviceAddress = gatt.device.address + Log.i(TAG, "Client: MTU changed for $deviceAddress to $mtu with status $status") + + if (status == BluetoothGatt.GATT_SUCCESS) { + Log.i(TAG, "MTU successfully negotiated for $deviceAddress. Discovering services.") + + // Now that MTU is set, connection is fully ready. + val deviceConn = BluetoothConnectionTracker.DeviceConnection( + device = gatt.device, + gatt = gatt, + rssi = rssi, + isClient = true + ) + connectionTracker.addDeviceConnection(deviceAddress, deviceConn) + + // Start service discovery only AFTER MTU is set. + gatt.discoverServices() + } else { + Log.w(TAG, "MTU negotiation failed for $deviceAddress with status: $status. Disconnecting.") + connectionTracker.removePendingConnection(deviceAddress) + gatt.disconnect() + } + } + + override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { + if (status == BluetoothGatt.GATT_SUCCESS) { + val service = gatt.getService(SERVICE_UUID) + if (service != null) { + val characteristic = service.getCharacteristic(CHARACTERISTIC_UUID) + if (characteristic != null) { + connectionTracker.getDeviceConnection(deviceAddress)?.let { deviceConn -> + val updatedConn = deviceConn.copy(characteristic = characteristic) + connectionTracker.updateDeviceConnection(deviceAddress, updatedConn) + Log.d(TAG, "Client: Updated device connection with characteristic for $deviceAddress") + } + + gatt.setCharacteristicNotification(characteristic, true) + val descriptor = characteristic.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")) + if (descriptor != null) { + descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE + gatt.writeDescriptor(descriptor) + + connectionScope.launch { + delay(200) + Log.i(TAG, "Client: Connection setup complete for $deviceAddress") + delegate?.onDeviceConnected(device) + } + } else { + Log.e(TAG, "Client: CCCD descriptor not found for $deviceAddress") + gatt.disconnect() + } + } else { + Log.e(TAG, "Client: Required characteristic not found for $deviceAddress") + gatt.disconnect() + } + } else { + Log.e(TAG, "Client: Required service not found for $deviceAddress") + gatt.disconnect() + } + } else { + Log.e(TAG, "Client: Service discovery failed with status $status for $deviceAddress") + gatt.disconnect() + } + } + + override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { + val value = characteristic.value + Log.d(TAG, "Client: Received packet from ${gatt.device.address}, size: ${value.size} bytes") + val packet = BitchatPacket.fromBinaryData(value) + if (packet != null) { + val peerID = String(packet.senderID).replace("\u0000", "") + Log.d(TAG, "Client: Parsed packet type ${packet.type} from $peerID") + delegate?.onPacketReceived(packet, peerID, gatt.device) + } else { + Log.w(TAG, "Client: Failed to parse packet from ${gatt.device.address}, size: ${value.size} bytes") + Log.w(TAG, "Client: Packet data: ${value.joinToString(" ") { "%02x".format(it) }}") + } + } + } + + try { + Log.d(TAG, "Client: Attempting GATT connection to $deviceAddress with autoConnect=false") + val gatt = device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE) + if (gatt == null) { + Log.e(TAG, "connectGatt returned null for $deviceAddress") + connectionTracker.removePendingConnection(deviceAddress) + } else { + Log.d(TAG, "Client: GATT connection initiated successfully for $deviceAddress") + } + } catch (e: Exception) { + Log.e(TAG, "Client: Exception connecting to $deviceAddress: ${e.message}") + connectionTracker.removePendingConnection(deviceAddress) + } + } + + /** + * Restart scanning for power mode changes + */ + fun restartScanning() { + if (!isActive) return + + connectionScope.launch { + stopScanning() + delay(1000) // Extra delay to avoid rate limiting + + if (powerManager.shouldUseDutyCycle()) { + Log.i(TAG, "Switching to duty cycle scanning mode") + // Duty cycle will handle scanning + } else { + Log.i(TAG, "Switching to continuous scanning mode") + startScanning() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothGattServerManager.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothGattServerManager.kt new file mode 100644 index 00000000..c73ea16c --- /dev/null +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothGattServerManager.kt @@ -0,0 +1,321 @@ +package com.bitchat.android.mesh + +import android.bluetooth.* +import android.bluetooth.le.AdvertiseCallback +import android.bluetooth.le.AdvertiseData +import android.bluetooth.le.AdvertiseSettings +import android.bluetooth.le.BluetoothLeAdvertiser +import android.content.Context +import android.os.ParcelUuid +import android.util.Log +import com.bitchat.android.protocol.BitchatPacket +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import java.util.* + +/** + * Manages GATT server operations, advertising, and server-side connections + */ +class BluetoothGattServerManager( + private val context: Context, + private val connectionScope: CoroutineScope, + private val connectionTracker: BluetoothConnectionTracker, + private val permissionManager: BluetoothPermissionManager, + private val powerManager: PowerManager, + private val delegate: BluetoothConnectionManagerDelegate? +) { + + companion object { + private const val TAG = "BluetoothGattServerManager" + // Use exact same UUIDs as iOS version + private val SERVICE_UUID = UUID.fromString("F47B5E2D-4A9E-4C5A-9B3F-8E1D2C3A4B5C") + private val CHARACTERISTIC_UUID = UUID.fromString("A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5D") + } + + // Core Bluetooth components + private val bluetoothManager: BluetoothManager = + context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager + private val bluetoothAdapter: BluetoothAdapter? = bluetoothManager.adapter + private val bleAdvertiser: BluetoothLeAdvertiser? = bluetoothAdapter?.bluetoothLeAdvertiser + + // GATT server for peripheral mode + private var gattServer: BluetoothGattServer? = null + private var characteristic: BluetoothGattCharacteristic? = null + private var advertiseCallback: AdvertiseCallback? = null + + // State management + private var isActive = false + + /** + * Start GATT server + */ + fun start(): Boolean { + if (!permissionManager.hasBluetoothPermissions()) { + Log.e(TAG, "Missing Bluetooth permissions") + return false + } + + if (bluetoothAdapter?.isEnabled != true) { + Log.e(TAG, "Bluetooth is not enabled") + return false + } + + if (bleAdvertiser == null) { + Log.e(TAG, "BLE advertiser not available") + return false + } + + isActive = true + + connectionScope.launch { + setupGattServer() + delay(300) // Brief delay to ensure GATT server is ready + startAdvertising() + } + + return true + } + + /** + * Stop GATT server + */ + fun stop() { + isActive = false + + connectionScope.launch { + stopAdvertising() + + // Close GATT server + gattServer?.close() + gattServer = null + + Log.i(TAG, "GATT server stopped") + } + } + + /** + * Get GATT server instance + */ + fun getGattServer(): BluetoothGattServer? = gattServer + + /** + * Get characteristic instance + */ + fun getCharacteristic(): BluetoothGattCharacteristic? = characteristic + + /** + * Setup GATT server with proper sequencing + */ + @Suppress("DEPRECATION") + private fun setupGattServer() { + if (!permissionManager.hasBluetoothPermissions()) return + + val serverCallback = object : BluetoothGattServerCallback() { + override fun onConnectionStateChange(device: BluetoothDevice, status: Int, newState: Int) { + // Guard against callbacks after service shutdown + if (!isActive) { + Log.d(TAG, "Server: Ignoring connection state change after shutdown") + return + } + + when (newState) { + BluetoothProfile.STATE_CONNECTED -> { + Log.i(TAG, "Server: Device connected ${device.address}") + val deviceConn = BluetoothConnectionTracker.DeviceConnection( + device = device, + isClient = false + ) + connectionTracker.addDeviceConnection(device.address, deviceConn) + } + BluetoothProfile.STATE_DISCONNECTED -> { + Log.i(TAG, "Server: Device disconnected ${device.address}") + connectionTracker.cleanupDeviceConnection(device.address) + } + } + } + + override fun onServiceAdded(status: Int, service: BluetoothGattService) { + // Guard against callbacks after service shutdown + if (!isActive) { + Log.d(TAG, "Server: Ignoring service added callback after shutdown") + return + } + + if (status == BluetoothGatt.GATT_SUCCESS) { + Log.d(TAG, "Server: Service added successfully: ${service.uuid}") + } else { + Log.e(TAG, "Server: Failed to add service: ${service.uuid}, status: $status") + } + } + + override fun onCharacteristicWriteRequest( + device: BluetoothDevice, + requestId: Int, + characteristic: BluetoothGattCharacteristic, + preparedWrite: Boolean, + responseNeeded: Boolean, + offset: Int, + value: ByteArray + ) { + // Guard against callbacks after service shutdown + if (!isActive) { + Log.d(TAG, "Server: Ignoring characteristic write after shutdown") + return + } + + if (characteristic.uuid == CHARACTERISTIC_UUID) { + Log.i(TAG, "Server: Received packet from ${device.address}, size: ${value.size} bytes") + val packet = BitchatPacket.fromBinaryData(value) + if (packet != null) { + val peerID = String(packet.senderID).replace("\u0000", "") + Log.d(TAG, "Server: Parsed packet type ${packet.type} from $peerID") + delegate?.onPacketReceived(packet, peerID, device) + } else { + Log.w(TAG, "Server: Failed to parse packet from ${device.address}, size: ${value.size} bytes") + Log.w(TAG, "Server: Packet data: ${value.joinToString(" ") { "%02x".format(it) }}") + } + + if (responseNeeded) { + gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, null) + } + } + } + + override fun onDescriptorWriteRequest( + device: BluetoothDevice, + requestId: Int, + descriptor: BluetoothGattDescriptor, + preparedWrite: Boolean, + responseNeeded: Boolean, + offset: Int, + value: ByteArray + ) { + // Guard against callbacks after service shutdown + if (!isActive) { + Log.d(TAG, "Server: Ignoring descriptor write after shutdown") + return + } + + if (BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE.contentEquals(value)) { + Log.d(TAG, "Device ${device.address} subscribed to notifications") + connectionTracker.addSubscribedDevice(device) + + connectionScope.launch { + delay(100) + if (isActive) { // Check if still active + delegate?.onDeviceConnected(device) + } + } + } + + if (responseNeeded) { + gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, null) + } + } + } + + // Proper cleanup sequencing to prevent race conditions + gattServer?.let { server -> + Log.d(TAG, "Cleaning up existing GATT server") + try { + server.close() + } catch (e: Exception) { + Log.w(TAG, "Error closing existing GATT server: ${e.message}") + } + } + + // Small delay to ensure cleanup is complete + Thread.sleep(100) + + if (!isActive) { + Log.d(TAG, "Service inactive, skipping GATT server creation") + return + } + + // Create new server + gattServer = bluetoothManager.openGattServer(context, serverCallback) + + // Create characteristic with notification support + characteristic = BluetoothGattCharacteristic( + CHARACTERISTIC_UUID, + BluetoothGattCharacteristic.PROPERTY_READ or + BluetoothGattCharacteristic.PROPERTY_WRITE or + BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE or + BluetoothGattCharacteristic.PROPERTY_NOTIFY, + BluetoothGattCharacteristic.PERMISSION_READ or + BluetoothGattCharacteristic.PERMISSION_WRITE + ) + + val descriptor = BluetoothGattDescriptor( + UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"), + BluetoothGattDescriptor.PERMISSION_READ or BluetoothGattDescriptor.PERMISSION_WRITE + ) + characteristic?.addDescriptor(descriptor) + + val service = BluetoothGattService(SERVICE_UUID, BluetoothGattService.SERVICE_TYPE_PRIMARY) + service.addCharacteristic(characteristic) + + gattServer?.addService(service) + + Log.i(TAG, "GATT server setup complete") + } + + /** + * Start advertising + */ + @Suppress("DEPRECATION") + private fun startAdvertising() { + if (!permissionManager.hasBluetoothPermissions() || bleAdvertiser == null || !isActive) return + + val settings = powerManager.getAdvertiseSettings() + + val data = AdvertiseData.Builder() + .addServiceUuid(ParcelUuid(SERVICE_UUID)) + .setIncludeTxPowerLevel(false) + .setIncludeDeviceName(false) + .build() + + advertiseCallback = object : AdvertiseCallback() { + override fun onStartSuccess(settingsInEffect: AdvertiseSettings) { + Log.i(TAG, "Advertising started (power mode: ${powerManager.getPowerInfo().split("Current Mode: ")[1].split("\n")[0]})") + } + + override fun onStartFailure(errorCode: Int) { + Log.e(TAG, "Advertising failed: $errorCode") + } + } + + try { + bleAdvertiser.startAdvertising(settings, data, advertiseCallback) + } catch (e: Exception) { + Log.e(TAG, "Exception starting advertising: ${e.message}") + } + } + + /** + * Stop advertising + */ + @Suppress("DEPRECATION") + private fun stopAdvertising() { + if (!permissionManager.hasBluetoothPermissions() || bleAdvertiser == null) return + try { + advertiseCallback?.let { bleAdvertiser.stopAdvertising(it) } + } catch (e: Exception) { + Log.w(TAG, "Error stopping advertising: ${e.message}") + } + } + + /** + * Restart advertising (for power mode changes) + */ + fun restartAdvertising() { + if (!isActive) return + + connectionScope.launch { + stopAdvertising() + delay(100) + startAdvertising() + } + } +} \ No newline at end of file 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 5e5c3136..803d65dd 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt @@ -521,6 +521,27 @@ class BluetoothMeshService(private val context: Context) { */ fun getPeerRSSI(): Map = peerManager.getAllPeerRSSI() + /** + * Get device address for a specific peer ID + */ + fun getDeviceAddressForPeer(peerID: String): String? { + return connectionManager.addressPeerMap.entries.find { it.value == peerID }?.key + } + + /** + * Get all device addresses mapped to their peer IDs + */ + fun getDeviceAddressToPeerMapping(): Map { + return connectionManager.addressPeerMap.toMap() + } + + /** + * Print device addresses for all connected peers + */ + fun printDeviceAddressesForPeers(): String { + return peerManager.getDebugInfoWithDeviceAddresses(connectionManager.addressPeerMap) + } + /** * Get debug status information */ @@ -531,7 +552,7 @@ class BluetoothMeshService(private val context: Context) { appendLine() appendLine(connectionManager.getDebugInfo()) appendLine() - appendLine(peerManager.getDebugInfo()) + appendLine(peerManager.getDebugInfo(connectionManager.addressPeerMap)) appendLine() appendLine(fragmentManager.getDebugInfo()) appendLine() diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothPacketBroadcaster.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothPacketBroadcaster.kt new file mode 100644 index 00000000..206865dd --- /dev/null +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothPacketBroadcaster.kt @@ -0,0 +1,179 @@ +package com.bitchat.android.mesh + +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothGatt +import android.bluetooth.BluetoothGattCharacteristic +import android.bluetooth.BluetoothGattServer +import android.util.Log +import com.bitchat.android.protocol.SpecialRecipients +import com.bitchat.android.model.RoutedPacket +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +/** + * Handles packet broadcasting to connected devices + */ +class BluetoothPacketBroadcaster( + private val connectionScope: CoroutineScope, + private val connectionTracker: BluetoothConnectionTracker +) { + + companion object { + private const val TAG = "BluetoothPacketBroadcaster" + private const val CLEANUP_DELAY = 500L + } + + fun broadcastPacket( + routed: RoutedPacket, + gattServer: BluetoothGattServer?, + characteristic: BluetoothGattCharacteristic? + ) { + val packet = routed.packet + val data = packet.toBinaryData() ?: return + // Check if we need to fragment + if (fragmentManager != null) { + val fragments = fragmentManager.createFragments(packet) + if (fragments.size > 1) { + Log.d(TAG, "Fragmenting packet into ${fragments.size} fragments") + connectionScope.launch { + fragments.forEach { fragment -> + broadcastSinglePacket(fragment, gattServer, characteristic) + // 20ms delay between fragments (matching iOS/Rust) + delay(20) + } + } + return + } + } + + // Send single packet if no fragmentation needed + broadcastSinglePacket(routed, gattServer, characteristic) + } + + + /** + * Broadcast single packet to connected devices with connection limit enforcement + */ + fun broadcastSinglePacket( + routed: RoutedPacket, + gattServer: BluetoothGattServer?, + characteristic: BluetoothGattCharacteristic? + ) { + val packet = routed.packet + val data = packet.toBinaryData() ?: return + + if (packet.recipientID != SpecialRecipients.BROADCAST) { + val recipientID = packet.recipientID?.let { + String(it).replace("\u0000", "").trim() + } ?: "" + + // Try to find the recipient in server connections (subscribedDevices) + val targetDevice = connectionTracker.getSubscribedDevices() + .firstOrNull { connectionTracker.addressPeerMap[it.address] == recipientID } + + // If found, send directly + if (targetDevice != null) { + Log.d(TAG, "Send packet type ${packet.type} directly to target device for recipient $recipientID: ${targetDevice.address}") + if (notifyDevice(targetDevice, data, gattServer, characteristic)) + return // Sent, no need to continue + } + + // Try to find the recipient in client connections (connectedDevices) + val targetDeviceConn = connectionTracker.getConnectedDevices().values + .firstOrNull { connectionTracker.addressPeerMap[it.device.address] == recipientID } + + // If found, send directly + if (targetDeviceConn != null) { + Log.d(TAG, "Send packet type ${packet.type} directly to target client connection for recipient $recipientID: ${targetDeviceConn.device.address}") + if (writeToDeviceConn(targetDeviceConn, data)) + return // Sent, no need to continue + } + } + + // Else, continue with broadcasting to all devices + val subscribedDevices = connectionTracker.getSubscribedDevices() + val connectedDevices = connectionTracker.getConnectedDevices() + + Log.d(TAG, "Broadcasting packet type ${packet.type} to ${subscribedDevices.size} server + ${connectedDevices.size} client connections") + + val senderID = String(packet.senderID).replace("\u0000", "") + + // Send to server connections (devices connected to our GATT server) + subscribedDevices.forEach { device -> + if (device.address == routed.relayAddress) { + Log.d(TAG, "Skipping broadcast back to relayer: ${device.address}") + return@forEach + } + if (connectionTracker.addressPeerMap[device.address] == senderID) { + Log.d(TAG, "Skipping broadcast back to sender: ${device.address}") + return@forEach + } + notifyDevice(device, data, gattServer, characteristic) + } + + // Send to client connections + connectedDevices.values.forEach { deviceConn -> + if (deviceConn.isClient && deviceConn.gatt != null && deviceConn.characteristic != null) { + if (deviceConn.device.address == routed.relayAddress) { + Log.d(TAG, "Skipping broadcast back to relayer: ${deviceConn.device.address}") + return@forEach + } + if (connectionTracker.addressPeerMap[deviceConn.device.address] == senderID) { + Log.d(TAG, "Skipping broadcast back to sender: ${deviceConn.device.address}") + return@forEach + } + writeToDeviceConn(deviceConn, data) + } + } + } + + /** + * Send data to a single device (server side) + */ + private fun notifyDevice( + device: BluetoothDevice, + data: ByteArray, + gattServer: BluetoothGattServer?, + characteristic: BluetoothGattCharacteristic? + ): Boolean { + return try { + characteristic?.let { char -> + char.value = data + val result = gattServer?.notifyCharacteristicChanged(device, char, false) ?: false + result + } ?: false + } catch (e: Exception) { + Log.w(TAG, "Error sending to server connection ${device.address}: ${e.message}") + connectionScope.launch { + delay(CLEANUP_DELAY) + connectionTracker.removeSubscribedDevice(device) + connectionTracker.addressPeerMap.remove(device.address) + } + false + } + } + + /** + * Send data to a single device (client side) + */ + private fun writeToDeviceConn( + deviceConn: BluetoothConnectionTracker.DeviceConnection, + data: ByteArray + ): Boolean { + return try { + deviceConn.characteristic?.let { char -> + char.value = data + val result = deviceConn.gatt?.writeCharacteristic(char) ?: false + result + } ?: false + } catch (e: Exception) { + Log.w(TAG, "Error sending to client connection ${deviceConn.device.address}: ${e.message}") + connectionScope.launch { + delay(CLEANUP_DELAY) + connectionTracker.cleanupDeviceConnection(deviceConn.device.address) + } + false + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothPermissionManager.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothPermissionManager.kt new file mode 100644 index 00000000..917de66e --- /dev/null +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothPermissionManager.kt @@ -0,0 +1,41 @@ +package com.bitchat.android.mesh + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import androidx.core.app.ActivityCompat + +/** + * Handles all Bluetooth permission checking logic + */ +class BluetoothPermissionManager(private val context: Context) { + + /** + * Check if all required Bluetooth permissions are granted + */ + fun hasBluetoothPermissions(): Boolean { + val permissions = mutableListOf() + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { + permissions.addAll(listOf( + Manifest.permission.BLUETOOTH_ADVERTISE, + Manifest.permission.BLUETOOTH_CONNECT, + Manifest.permission.BLUETOOTH_SCAN + )) + } else { + permissions.addAll(listOf( + Manifest.permission.BLUETOOTH, + Manifest.permission.BLUETOOTH_ADMIN + )) + } + + permissions.addAll(listOf( + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACCESS_FINE_LOCATION + )) + + return permissions.all { + ActivityCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bitchat/android/mesh/PeerManager.kt b/app/src/main/java/com/bitchat/android/mesh/PeerManager.kt index eeeb8ba7..a1234509 100644 --- a/app/src/main/java/com/bitchat/android/mesh/PeerManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/PeerManager.kt @@ -183,7 +183,7 @@ class PeerManager { /** * Get debug information */ - fun getDebugInfo(): String { + fun getDebugInfo(addressPeerMap: Map? = null): String { return buildString { appendLine("=== Peer Manager Debug Info ===") appendLine("Active Peers: ${activePeers.size}") @@ -191,13 +191,39 @@ class PeerManager { val nickname = peerNicknames[peerID] ?: "Unknown" val timeSince = (System.currentTimeMillis() - lastSeen) / 1000 val rssi = peerRSSI[peerID]?.let { "${it} dBm" } ?: "No RSSI" - appendLine(" - $peerID ($nickname) - last seen ${timeSince}s ago, RSSI: $rssi") + + // Find device address for this peer ID + val deviceAddress = addressPeerMap?.entries?.find { it.value == peerID }?.key + val addressInfo = deviceAddress?.let { " [Device: $it]" } ?: " [Device: Unknown]" + + appendLine(" - $peerID ($nickname)$addressInfo - last seen ${timeSince}s ago, RSSI: $rssi") } appendLine("Announced Peers: ${announcedPeers.size}") appendLine("Announced To Peers: ${announcedToPeers.size}") } } + /** + * Get debug information with device addresses + */ + fun getDebugInfoWithDeviceAddresses(addressPeerMap: Map): String { + return buildString { + appendLine("=== Device Address to Peer Mapping ===") + if (addressPeerMap.isEmpty()) { + appendLine("No device address mappings available") + } else { + addressPeerMap.forEach { (deviceAddress, peerID) -> + val nickname = peerNicknames[peerID] ?: "Unknown" + val isActive = activePeers.containsKey(peerID) + val status = if (isActive) "ACTIVE" else "INACTIVE" + appendLine(" Device: $deviceAddress -> Peer: $peerID ($nickname) [$status]") + } + } + appendLine() + appendLine(getDebugInfo(addressPeerMap)) + } + } + /** * Notify delegate of peer list updates */