From d962ed8ccbf446eba08e594480c49ded9aed9f98 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 9 Jul 2025 15:04:43 +0200 Subject: [PATCH 1/8] logging --- .../android/mesh/BluetoothMeshService.kt | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) 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 065f775e..b6a9a9ff 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt @@ -55,6 +55,26 @@ class BluetoothMeshService(private val context: Context) { init { setupDelegates() + startPeriodicDebugLogging() + } + + /** + * Start periodic debug logging every 10 seconds + */ + private fun startPeriodicDebugLogging() { + serviceScope.launch { + while (isActive) { + try { + delay(10000) // 10 seconds + val debugInfo = getDebugStatus() + Log.d(TAG, "=== PERIODIC DEBUG STATUS ===") + Log.d(TAG, debugInfo) + Log.d(TAG, "=== END DEBUG STATUS ===") + } catch (e: Exception) { + Log.e(TAG, "Error in periodic debug logging: ${e.message}") + } + } + } } /** From d3e258b44277a6c083aaa4245b98ca33007dadd2 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 9 Jul 2025 15:34:20 +0200 Subject: [PATCH 2/8] precvent duplicate connections --- .../mesh/BluetoothConnectionManager.kt | 217 ++++++++++++++---- 1 file changed, 178 insertions(+), 39 deletions(-) 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 3bdaa842..7a951880 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt @@ -44,6 +44,17 @@ class BluetoothConnectionManager(private val context: Context, private val myPee private val gattConnections = ConcurrentHashMap() // Track GATT client connections private val peripheralRSSI = ConcurrentHashMap() // Track RSSI by device address during discovery + // FIXED: Track connection attempts to prevent duplicates + private val pendingConnections = ConcurrentHashMap() // device address -> attempt timestamp + private val connectionAttempts = ConcurrentHashMap() // device address -> attempt count + private val maxConnectionAttempts = 3 + private val connectionRetryDelay = 5000L // 5 seconds + + // Service registration tracking + private var serviceRegistered = false + private var advertisingActive = false + private var scanningActive = false + // Delegate for callbacks var delegate: BluetoothConnectionManagerDelegate? = null @@ -194,6 +205,9 @@ class BluetoothConnectionManager(private val context: Context, private val myPee appendLine("BLE Scanner Available: ${bleScanner != null}") appendLine("BLE Advertiser Available: ${bleAdvertiser != null}") appendLine("GATT Server Active: ${gattServer != null}") + appendLine("Service Registered: $serviceRegistered") + appendLine("Advertising Active: $advertisingActive") + appendLine("Scanning Active: $scanningActive") appendLine() appendLine("Connected Devices: ${connectedDevices.size}") connectedDevices.forEach { (address, device) -> @@ -210,6 +224,17 @@ class BluetoothConnectionManager(private val context: Context, private val myPee appendLine(" - ${device.address}") } appendLine() + appendLine("Pending Connections: ${pendingConnections.size}") + pendingConnections.forEach { (address, timestamp) -> + val elapsed = (System.currentTimeMillis() - timestamp) / 1000 + appendLine(" - $address (${elapsed}s ago)") + } + appendLine() + appendLine("Connection Attempts: ${connectionAttempts.size}") + connectionAttempts.forEach { (address, attempts) -> + appendLine(" - $address: $attempts attempts") + } + appendLine() appendLine("Peripheral RSSI: ${peripheralRSSI.size}") peripheralRSSI.forEach { (address, rssi) -> appendLine(" - $address: $rssi dBm") @@ -514,94 +539,205 @@ class BluetoothConnectionManager(private val context: Context, private val myPee } /** - * Handle scan result and connect to discovered devices + * Handle scan result and connect to discovered devices - FIXED to prevent duplicate connections */ @Suppress("DEPRECATION") private fun handleScanResult(result: ScanResult) { val device = result.device val rssi = result.rssi + val deviceAddress = device.address // Filter out weak signals if (rssi < -90) { return } - // Check if already connected - if (connectedDevices.values.any { it.address == device.address }) { + // FIXED: Check for existing client connections only, allow server connections + if (gattConnections.containsKey(device)) { + Log.d(TAG, "Already have client connection to $deviceAddress") return } - // Store RSSI - peripheralRSSI[device.address] = rssi + // FIXED: Check for pending connections to prevent duplicates + val currentTime = System.currentTimeMillis() + val pendingTime = pendingConnections[deviceAddress] + if (pendingTime != null && (currentTime - pendingTime) < connectionRetryDelay) { + Log.d(TAG, "Connection attempt to $deviceAddress already pending, skipping") + return + } - Log.i(TAG, "Found bitchat service at ${device.address} (RSSI: $rssi), connecting...") + // Check connection attempt limits + val attempts = connectionAttempts.getOrDefault(deviceAddress, 0) + if (attempts >= maxConnectionAttempts) { + if (pendingTime == null || (currentTime - pendingTime) > connectionRetryDelay * 2) { + // Reset after extended delay + connectionAttempts[deviceAddress] = 0 + pendingConnections.remove(deviceAddress) + Log.d(TAG, "Reset connection attempts for $deviceAddress after extended delay") + } else { + Log.d(TAG, "Max connection attempts reached for $deviceAddress") + return + } + } + + // Store RSSI and track connection attempt + peripheralRSSI[deviceAddress] = rssi + pendingConnections[deviceAddress] = currentTime + connectionAttempts[deviceAddress] = attempts + 1 + + Log.i(TAG, "Found bitchat service at $deviceAddress (RSSI: $rssi), connecting (attempt ${attempts + 1})...") // Connect to device - connectToDevice(device) + connectToDeviceFixed(device) } /** - * Connect to a discovered device + * Connect to a discovered device - FIXED version with proper timeout and cleanup handling */ @Suppress("DEPRECATION") - private fun connectToDevice(device: BluetoothDevice) { + private fun connectToDeviceFixed(device: BluetoothDevice) { if (!hasBluetoothPermissions()) return + val deviceAddress = device.address + Log.d(TAG, "Attempting to connect to $deviceAddress") + val gattCallback = object : BluetoothGattCallback() { override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { + val statusMessage = when (status) { + BluetoothGatt.GATT_SUCCESS -> "SUCCESS" + 147 -> "CONNECTION_TIMEOUT" + 133 -> "GATT_ERROR" + 8 -> "CONNECTION_TIMEOUT" + else -> "ERROR_$status" + } + + Log.d(TAG, "onConnectionStateChange: $deviceAddress status=$statusMessage($status) newState=$newState") + when (newState) { BluetoothProfile.STATE_CONNECTED -> { - Log.d(TAG, "Connected to ${gatt.device.address} as client") - connectedDevices[gatt.device.address] = gatt.device - gattConnections[gatt.device] = gatt - gatt.discoverServices() + if (status == BluetoothGatt.GATT_SUCCESS) { + Log.i(TAG, "Successfully connected to $deviceAddress as client") + connectedDevices[deviceAddress] = gatt.device + gattConnections[gatt.device] = gatt + pendingConnections.remove(deviceAddress) // Clear pending connection + + // Discover services with a slight delay for stability + connectionScope.launch { + delay(200) // Small delay for connection stabilization + if (gattConnections.containsKey(gatt.device)) { + gatt.discoverServices() + } + } + } else { + Log.e(TAG, "Connected to $deviceAddress with error status: $status") + gatt.disconnect() + } } BluetoothProfile.STATE_DISCONNECTED -> { - Log.d(TAG, "Disconnected from ${gatt.device.address}") - connectedDevices.remove(gatt.device.address) + Log.d(TAG, "Disconnected from $deviceAddress (status: $status)") + connectedDevices.remove(deviceAddress) deviceCharacteristics.remove(gatt.device) gattConnections.remove(gatt.device) - gatt.close() - } - } - } - - override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { - if (status == BluetoothGatt.GATT_SUCCESS) { - val service = gatt.getService(SERVICE_UUID) - val characteristic = service?.getCharacteristic(CHARACTERISTIC_UUID) - - if (characteristic != null) { - deviceCharacteristics[gatt.device] = characteristic - gatt.setCharacteristicNotification(characteristic, true) + pendingConnections.remove(deviceAddress) // Clear pending connection - // Enable notifications - val descriptor = characteristic.getDescriptor( - UUID.fromString("00002902-0000-1000-8000-00805f9b34fb") - ) - descriptor?.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE - gatt.writeDescriptor(descriptor) + // Handle connection errors + if (status == 147 || status == 133 || status == 8) { // Timeout or connection errors + Log.w(TAG, "Connection failed to $deviceAddress with status $status") + } - // Notify delegate - connectionScope.launch { - delay(200) - delegate?.onDeviceConnected(gatt.device) + // Close the GATT connection + try { + gatt.close() + } catch (e: Exception) { + Log.e(TAG, "Error closing GATT connection to $deviceAddress: ${e.message}") } } } } + override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { + Log.d(TAG, "onServicesDiscovered: $deviceAddress status=$status") + + if (status == BluetoothGatt.GATT_SUCCESS) { + val service = gatt.getService(SERVICE_UUID) + val characteristic = service?.getCharacteristic(CHARACTERISTIC_UUID) + + if (characteristic != null) { + Log.d(TAG, "Found bitchat service and characteristic on $deviceAddress") + deviceCharacteristics[gatt.device] = characteristic + + // Enable notifications + val notificationSuccess = gatt.setCharacteristicNotification(characteristic, true) + Log.d(TAG, "Set notification for $deviceAddress: $notificationSuccess") + + // Write notification descriptor + val descriptor = characteristic.getDescriptor( + UUID.fromString("00002902-0000-1000-8000-00805f9b34fb") + ) + if (descriptor != null) { + descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE + val descriptorSuccess = gatt.writeDescriptor(descriptor) + Log.d(TAG, "Write notification descriptor for $deviceAddress: $descriptorSuccess") + } + + // Notify delegate about successful connection + connectionScope.launch { + delay(100) // Ensure everything is set up + delegate?.onDeviceConnected(gatt.device) + } + } else { + Log.e(TAG, "Bitchat service or characteristic not found on $deviceAddress") + gatt.disconnect() + } + } else { + Log.e(TAG, "Service discovery failed on $deviceAddress with status: $status") + gatt.disconnect() + } + } + override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { val value = characteristic.value + Log.d(TAG, "Received ${value.size} bytes from $deviceAddress") + val packet = BitchatPacket.fromBinaryData(value) if (packet != null) { val peerID = String(packet.senderID).replace("\u0000", "") delegate?.onPacketReceived(packet, peerID, gatt.device) + } else { + Log.w(TAG, "Failed to parse packet from $deviceAddress") + } + } + + override fun onDescriptorWrite(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) { + Log.d(TAG, "onDescriptorWrite: $deviceAddress status=$status") + if (status == BluetoothGatt.GATT_SUCCESS) { + Log.i(TAG, "Successfully enabled notifications for $deviceAddress") + } else { + Log.e(TAG, "Failed to enable notifications for $deviceAddress: $status") } } } - device.connectGatt(context, false, gattCallback) + // Attempt the connection + try { + val gatt = device.connectGatt(context, false, gattCallback) + if (gatt == null) { + Log.e(TAG, "Failed to create GATT connection to $deviceAddress") + pendingConnections.remove(deviceAddress) + } + } catch (e: Exception) { + Log.e(TAG, "Exception connecting to $deviceAddress: ${e.message}") + pendingConnections.remove(deviceAddress) + } + } + + /** + * Connect to a discovered device - LEGACY METHOD (kept for compatibility) + */ + @Suppress("DEPRECATION") + private fun connectToDevice(device: BluetoothDevice) { + // Redirect to fixed method + connectToDeviceFixed(device) } /** @@ -613,6 +749,9 @@ class BluetoothConnectionManager(private val context: Context, private val myPee subscribedDevices.clear() gattConnections.clear() peripheralRSSI.clear() + // FIXED: Clear new tracking maps + pendingConnections.clear() + connectionAttempts.clear() } } From dd8aa083b9db415a9394d6f04029250543aa5f67 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 9 Jul 2025 15:47:54 +0200 Subject: [PATCH 3/8] power saving --- .../java/com/bitchat/android/MainActivity.kt | 15 + .../mesh/BluetoothConnectionManager.kt | 667 +++++++++--------- .../android/mesh/BluetoothMeshService.kt | 2 +- .../com/bitchat/android/mesh/PowerManager.kt | 337 +++++++++ .../com/bitchat/android/ui/ChatViewModel.kt | 5 + 5 files changed, 675 insertions(+), 351 deletions(-) create mode 100644 app/src/main/java/com/bitchat/android/mesh/PowerManager.kt diff --git a/app/src/main/java/com/bitchat/android/MainActivity.kt b/app/src/main/java/com/bitchat/android/MainActivity.kt index 30a8f2fb..4d7091e6 100644 --- a/app/src/main/java/com/bitchat/android/MainActivity.kt +++ b/app/src/main/java/com/bitchat/android/MainActivity.kt @@ -15,9 +15,12 @@ import androidx.compose.material3.Surface import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.lifecycle.lifecycleScope import com.bitchat.android.ui.ChatScreen import com.bitchat.android.ui.ChatViewModel import com.bitchat.android.ui.theme.BitchatTheme +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch class MainActivity : ComponentActivity() { @@ -53,6 +56,18 @@ class MainActivity : ComponentActivity() { } } + override fun onResume() { + super.onResume() + // Notify that app is in foreground for power optimization + chatViewModel.setAppBackgroundState(false) + } + + override fun onPause() { + super.onPause() + // Notify that app is in background for power optimization + chatViewModel.setAppBackgroundState(true) + } + private fun requestPermissions() { val permissions = mutableListOf() 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 7a951880..efa8dada 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt @@ -15,45 +15,52 @@ import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.CopyOnWriteArrayList /** - * Manages Bluetooth connections, advertising, and scanning - * Extracted from BluetoothMeshService for better separation of concerns + * Power-optimized Bluetooth connection manager with comprehensive memory management + * Integrates with PowerManager for adaptive power consumption */ -class BluetoothConnectionManager(private val context: Context, private val myPeerID: String) { +class BluetoothConnectionManager( + private val context: Context, + private val myPeerID: String +) : PowerManagerDelegate { companion object { private const val TAG = "BluetoothConnectionManager" - // Use exact same UUIDs as iOS version and original service + // 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 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 - // Connection tracking - FIXED to properly track both server and client connections - private val connectedDevices = ConcurrentHashMap() - private val deviceCharacteristics = ConcurrentHashMap() + // Simplified connection tracking - reduced memory footprint + private val connectedDevices = ConcurrentHashMap() private val subscribedDevices = CopyOnWriteArrayList() - private val gattConnections = ConcurrentHashMap() // Track GATT client connections - private val peripheralRSSI = ConcurrentHashMap() // Track RSSI by device address during discovery - // FIXED: Track connection attempts to prevent duplicates - private val pendingConnections = ConcurrentHashMap() // device address -> attempt timestamp - private val connectionAttempts = ConcurrentHashMap() // device address -> attempt count - private val maxConnectionAttempts = 3 - private val connectionRetryDelay = 5000L // 5 seconds + // Connection attempt tracking with automatic cleanup + private val pendingConnections = ConcurrentHashMap() - // Service registration tracking - private var serviceRegistered = false - private var advertisingActive = false - private var scanningActive = false + // Service state + private var isActive = false + private var scanCallback: ScanCallback? = null + private var advertiseCallback: AdvertiseCallback? = null // Delegate for callbacks var delegate: BluetoothConnectionManagerDelegate? = null @@ -62,10 +69,41 @@ class BluetoothConnectionManager(private val context: Context, private val myPee private val connectionScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) /** - * Start all Bluetooth services + * 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 + } + + init { + powerManager.delegate = this + } + + /** + * Start all Bluetooth services with power optimization */ fun startServices(): Boolean { - Log.i(TAG, "Starting Bluetooth services...") + Log.i(TAG, "Starting power-optimized Bluetooth services...") if (!hasBluetoothPermissions()) { Log.e(TAG, "Missing Bluetooth permissions") @@ -83,19 +121,26 @@ class BluetoothConnectionManager(private val context: Context, private val myPee } try { + isActive = true setupGattServer() - // Start services in sequence + // Start power manager and services connectionScope.launch { + powerManager.start() delay(500) // Ensure GATT server is ready startAdvertising() delay(200) - startScanning() - delay(200) + if (powerManager.shouldUseDutyCycle()) { + Log.i(TAG, "Using power-aware duty cycling") + } else { + startScanning() + } - Log.i(TAG, "All Bluetooth services started successfully") + startPeriodicCleanup() + + Log.i(TAG, "Power-optimized Bluetooth services started successfully") } return true @@ -107,82 +152,85 @@ class BluetoothConnectionManager(private val context: Context, private val myPee } /** - * Stop all Bluetooth services + * Stop all Bluetooth services with proper cleanup */ fun stopServices() { - Log.i(TAG, "Stopping Bluetooth services") + Log.i(TAG, "Stopping power-optimized Bluetooth services") + + isActive = false connectionScope.launch { - // Cleanup all GATT client connections - gattConnections.values.forEach { gatt -> - try { - gatt.disconnect() - gatt.close() - } catch (e: Exception) { - Log.w(TAG, "Error closing GATT connection: ${e.message}") - } - } + // Stop power manager first + powerManager.stop() - // Stop advertising and scanning - stopAdvertising() + // Stop scanning and advertising stopScanning() + stopAdvertising() + + // Cleanup all GATT connections with delay + cleanupAllConnections() // Close GATT server gattServer?.close() + gattServer = null - // Clear all connection tracking - connectedDevices.clear() - deviceCharacteristics.clear() - subscribedDevices.clear() - gattConnections.clear() - peripheralRSSI.clear() + // Clear tracking + clearAllConnections() connectionScope.cancel() + + Log.i(TAG, "All Bluetooth services stopped") } } /** - * Broadcast packet to all connected devices + * Set app background state for power optimization + */ + fun setAppBackgroundState(inBackground: Boolean) { + powerManager.setAppBackgroundState(inBackground) + } + + /** + * Broadcast packet to connected devices with connection limit enforcement */ fun broadcastPacket(packet: BitchatPacket) { + if (!isActive) return + val data = packet.toBinaryData() ?: return - Log.d(TAG, "Broadcasting packet type ${packet.type} to ${subscribedDevices.size} server connections and ${gattConnections.size} client connections") + Log.d(TAG, "Broadcasting packet type ${packet.type} to ${subscribedDevices.size} server + ${connectedDevices.size} client connections") - // Send to devices connected to our GATT server + // Send to server connections (devices connected to our GATT server) subscribedDevices.forEach { device -> try { characteristic?.let { char -> char.value = data - val success = gattServer?.notifyCharacteristicChanged(device, char, false) ?: false - if (success) { - Log.d(TAG, "Sent packet to server connection: ${device.address}") - } else { - Log.w(TAG, "Failed to send packet to server connection: ${device.address}") - } + gattServer?.notifyCharacteristicChanged(device, char, false) } } catch (e: Exception) { - Log.e(TAG, "Error sending to server connection ${device.address}: ${e.message}") + Log.w(TAG, "Error sending to server connection ${device.address}: ${e.message}") + // Clean up failed connection + connectionScope.launch { + delay(CLEANUP_DELAY) + subscribedDevices.remove(device) + } } } - // Send to devices we are connected to as a client - gattConnections.forEach { (device, gatt) -> - try { - val characteristic = deviceCharacteristics[device] - if (characteristic != null) { - characteristic.value = data - val success = gatt.writeCharacteristic(characteristic) - if (success) { - Log.d(TAG, "Sent packet to client connection: ${device.address}") - } else { - Log.w(TAG, "Failed to send packet to client connection: ${device.address}") + // Send to client connections + connectedDevices.values.forEach { deviceConn -> + if (deviceConn.isClient && deviceConn.gatt != null && deviceConn.characteristic != null) { + try { + deviceConn.characteristic.value = data + deviceConn.gatt.writeCharacteristic(deviceConn.characteristic) + } catch (e: Exception) { + Log.w(TAG, "Error sending to client connection ${deviceConn.device.address}: ${e.message}") + // Clean up failed connection + connectionScope.launch { + delay(CLEANUP_DELAY) + cleanupDeviceConnection(deviceConn.device.address) } - } else { - Log.w(TAG, "No characteristic found for client connection: ${device.address}") } - } catch (e: Exception) { - Log.e(TAG, "Error sending to client connection ${device.address}: ${e.message}") } } } @@ -190,61 +238,71 @@ class BluetoothConnectionManager(private val context: Context, private val myPee /** * Get connected device count */ - fun getConnectedDeviceCount(): Int { - return connectedDevices.size - } + fun getConnectedDeviceCount(): Int = connectedDevices.size /** - * Get debug information + * Get debug information including power management */ fun getDebugInfo(): String { return buildString { - appendLine("=== Bluetooth Connection Manager Debug Info ===") + appendLine("=== Power-Optimized Bluetooth Connection Manager ===") + appendLine("Active: $isActive") appendLine("Bluetooth Enabled: ${bluetoothAdapter?.isEnabled}") appendLine("Has Permissions: ${hasBluetoothPermissions()}") - appendLine("BLE Scanner Available: ${bleScanner != null}") - appendLine("BLE Advertiser Available: ${bleAdvertiser != null}") appendLine("GATT Server Active: ${gattServer != null}") - appendLine("Service Registered: $serviceRegistered") - appendLine("Advertising Active: $advertisingActive") - appendLine("Scanning Active: $scanningActive") appendLine() - appendLine("Connected Devices: ${connectedDevices.size}") - connectedDevices.forEach { (address, device) -> - appendLine(" - $address") - } + appendLine(powerManager.getPowerInfo()) appendLine() - appendLine("GATT Client Connections: ${gattConnections.size}") - gattConnections.keys.forEach { device -> - appendLine(" - ${device.address}") + 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}") - subscribedDevices.forEach { device -> - appendLine(" - ${device.address}") - } appendLine() appendLine("Pending Connections: ${pendingConnections.size}") - pendingConnections.forEach { (address, timestamp) -> - val elapsed = (System.currentTimeMillis() - timestamp) / 1000 - appendLine(" - $address (${elapsed}s ago)") - } - appendLine() - appendLine("Connection Attempts: ${connectionAttempts.size}") - connectionAttempts.forEach { (address, attempts) -> - appendLine(" - $address: $attempts attempts") - } - appendLine() - appendLine("Peripheral RSSI: ${peripheralRSSI.size}") - peripheralRSSI.forEach { (address, rssi) -> - appendLine(" - $address: $rssi dBm") + val now = System.currentTimeMillis() + pendingConnections.forEach { (address, attempt) -> + val elapsed = (now - attempt.lastAttempt) / 1000 + appendLine(" - $address: ${attempt.attempts} attempts, last ${elapsed}s ago") } } } - /** - * Check if we have the required Bluetooth permissions - */ + // MARK: - PowerManagerDelegate Implementation + + override fun onPowerModeChanged(newMode: PowerManager.PowerMode) { + Log.i(TAG, "Power mode changed to: $newMode") + + connectionScope.launch { + // Update advertising and scanning based on new power mode + stopAdvertising() + delay(100) + startAdvertising() + + // Restart scanning with new settings if not using duty cycle + if (!powerManager.shouldUseDutyCycle()) { + stopScanning() + delay(100) + startScanning() + } + + // Enforce connection limits + enforceConnectionLimits() + } + } + + override fun onScanStateChanged(shouldScan: Boolean) { + if (shouldScan) { + startScanning() + } else { + stopScanning() + } + } + + // MARK: - Private Implementation + private fun hasBluetoothPermissions(): Boolean { val permissions = mutableListOf() @@ -271,9 +329,6 @@ class BluetoothConnectionManager(private val context: Context, private val myPee } } - /** - * Setup GATT server for peripheral mode - */ @Suppress("DEPRECATION") private fun setupGattServer() { if (!hasBluetoothPermissions()) return @@ -282,14 +337,16 @@ class BluetoothConnectionManager(private val context: Context, private val myPee override fun onConnectionStateChange(device: BluetoothDevice, status: Int, newState: Int) { when (newState) { BluetoothProfile.STATE_CONNECTED -> { - Log.d(TAG, "Device connected to server: ${device.address}") - connectedDevices[device.address] = device + 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, "Device disconnected from server: ${device.address}") - connectedDevices.remove(device.address) - deviceCharacteristics.remove(device) - subscribedDevices.remove(device) + Log.d(TAG, "Server: Device disconnected ${device.address}") + cleanupDeviceConnection(device.address) } } } @@ -304,8 +361,6 @@ class BluetoothConnectionManager(private val context: Context, private val myPee value: ByteArray ) { if (characteristic.uuid == CHARACTERISTIC_UUID) { - Log.d(TAG, "Received write request from ${device.address}, ${value.size} bytes") - val packet = BitchatPacket.fromBinaryData(value) if (packet != null) { val peerID = String(packet.senderID).replace("\u0000", "") @@ -329,14 +384,11 @@ class BluetoothConnectionManager(private val context: Context, private val myPee ) { if (BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE.contentEquals(value)) { Log.d(TAG, "Device ${device.address} subscribed to notifications") - if (!subscribedDevices.contains(device)) { - subscribedDevices.add(device) - - // Notify delegate about new connection - connectionScope.launch { - delay(100) // Ensure connection is stable - delegate?.onDeviceConnected(device) - } + subscribedDevices.add(device) + + connectionScope.launch { + delay(100) + delegate?.onDeviceConnected(device) } } @@ -346,13 +398,12 @@ class BluetoothConnectionManager(private val context: Context, private val myPee } } - // Clean up any existing GATT server + // Clean up existing server gattServer?.close() - clearAllConnections() gattServer = bluetoothManager.openGattServer(context, serverCallback) - // Create characteristic + // Create characteristic with notification support characteristic = BluetoothGattCharacteristic( CHARACTERISTIC_UUID, BluetoothGattCharacteristic.PROPERTY_READ or @@ -363,14 +414,12 @@ class BluetoothConnectionManager(private val context: Context, private val myPee BluetoothGattCharacteristic.PERMISSION_WRITE ) - // Add notification descriptor val descriptor = BluetoothGattDescriptor( UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"), BluetoothGattDescriptor.PERMISSION_READ or BluetoothGattDescriptor.PERMISSION_WRITE ) characteristic?.addDescriptor(descriptor) - // Create service val service = BluetoothGattService(SERVICE_UUID, BluetoothGattService.SERVICE_TYPE_PRIMARY) service.addCharacteristic(characteristic) @@ -379,49 +428,25 @@ class BluetoothConnectionManager(private val context: Context, private val myPee Log.i(TAG, "GATT server setup complete") } - /** - * Start BLE advertising - */ @Suppress("DEPRECATION") private fun startAdvertising() { - if (!hasBluetoothPermissions() || bleAdvertiser == null) { - Log.e(TAG, "Cannot start advertising: missing permissions or advertiser unavailable") - return - } + if (!hasBluetoothPermissions() || bleAdvertiser == null || !isActive) return - val settings = AdvertiseSettings.Builder() - .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED) - .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM) - .setConnectable(true) - .setTimeout(0) - .build() + val settings = powerManager.getAdvertiseSettings() val data = AdvertiseData.Builder() .addServiceUuid(ParcelUuid(SERVICE_UUID)) - .addServiceData(ParcelUuid(SERVICE_UUID), myPeerID.toByteArray(Charsets.UTF_8)) .setIncludeTxPowerLevel(false) .setIncludeDeviceName(false) .build() - val advertiseCallback = object : AdvertiseCallback() { + advertiseCallback = object : AdvertiseCallback() { override fun onStartSuccess(settingsInEffect: AdvertiseSettings) { - Log.i(TAG, "Advertising started successfully with peer ID: $myPeerID") + Log.i(TAG, "Advertising started (power mode: ${powerManager.getPowerInfo().split("Current Mode: ")[1].split("\n")[0]})") } override fun onStartFailure(errorCode: Int) { - val errorMessage = when (errorCode) { - ADVERTISE_FAILED_ALREADY_STARTED -> "Already started" - ADVERTISE_FAILED_DATA_TOO_LARGE -> "Data too large" - ADVERTISE_FAILED_FEATURE_UNSUPPORTED -> "Feature unsupported" - ADVERTISE_FAILED_INTERNAL_ERROR -> "Internal error" - ADVERTISE_FAILED_TOO_MANY_ADVERTISERS -> "Too many advertisers" - else -> "Unknown error: $errorCode" - } - Log.e(TAG, "Advertising failed: $errorMessage") - - if (errorCode == ADVERTISE_FAILED_DATA_TOO_LARGE) { - startMinimalAdvertising() - } + Log.e(TAG, "Advertising failed: $errorCode") } } @@ -432,75 +457,25 @@ class BluetoothConnectionManager(private val context: Context, private val myPee } } - /** - * Fallback minimal advertising - */ - @Suppress("DEPRECATION") - private fun startMinimalAdvertising() { - if (!hasBluetoothPermissions() || bleAdvertiser == null) return - - val settings = AdvertiseSettings.Builder() - .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED) - .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM) - .setConnectable(true) - .setTimeout(0) - .build() - - val data = AdvertiseData.Builder() - .setIncludeDeviceName(false) - .setIncludeTxPowerLevel(false) - .addServiceUuid(ParcelUuid(SERVICE_UUID)) - .build() - - val advertiseCallback = object : AdvertiseCallback() { - override fun onStartSuccess(settingsInEffect: AdvertiseSettings) { - Log.i(TAG, "Minimal advertising started successfully") - } - - override fun onStartFailure(errorCode: Int) { - Log.e(TAG, "Even minimal advertising failed: $errorCode") - } - } - - try { - bleAdvertiser.startAdvertising(settings, data, advertiseCallback) - } catch (e: Exception) { - Log.e(TAG, "Exception starting minimal advertising: ${e.message}") - } - } - - /** - * Stop BLE advertising - */ @Suppress("DEPRECATION") private fun stopAdvertising() { if (!hasBluetoothPermissions() || bleAdvertiser == null) return - bleAdvertiser.stopAdvertising(object : AdvertiseCallback() {}) + try { + advertiseCallback?.let { bleAdvertiser.stopAdvertising(it) } + } catch (e: Exception) { + Log.w(TAG, "Error stopping advertising: ${e.message}") + } } - /** - * Start BLE scanning - */ @Suppress("DEPRECATION") private fun startScanning() { - if (!hasBluetoothPermissions() || bleScanner == null) { - Log.e(TAG, "Cannot start scanning: missing permissions or scanner unavailable") - return - } + if (!hasBluetoothPermissions() || bleScanner == null || !isActive) return val scanFilter = ScanFilter.Builder() .setServiceUuid(ParcelUuid(SERVICE_UUID)) .build() - val scanSettings = ScanSettings.Builder() - .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) - .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) - .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE) - .setNumOfMatches(ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT) - .setReportDelay(10) - .build() - - val scanCallback = object : ScanCallback() { + scanCallback = object : ScanCallback() { override fun onScanResult(callbackType: Int, result: ScanResult) { handleScanResult(result) } @@ -510,248 +485,240 @@ class BluetoothConnectionManager(private val context: Context, private val myPee } override fun onScanFailed(errorCode: Int) { - val errorMessage = when (errorCode) { - SCAN_FAILED_ALREADY_STARTED -> "Already started" - SCAN_FAILED_APPLICATION_REGISTRATION_FAILED -> "App registration failed" - SCAN_FAILED_FEATURE_UNSUPPORTED -> "Feature unsupported" - SCAN_FAILED_INTERNAL_ERROR -> "Internal error" - else -> "Unknown error: $errorCode" - } - Log.e(TAG, "Scan failed: $errorMessage") + Log.e(TAG, "Scan failed: $errorCode") } } try { - bleScanner.startScan(listOf(scanFilter), scanSettings, scanCallback) - Log.i(TAG, "Started BLE scanning") + bleScanner.startScan(listOf(scanFilter), powerManager.getScanSettings(), scanCallback) } catch (e: Exception) { Log.e(TAG, "Exception starting scan: ${e.message}") } } - /** - * Stop BLE scanning - */ @Suppress("DEPRECATION") private fun stopScanning() { if (!hasBluetoothPermissions() || bleScanner == null) return - bleScanner.stopScan(object : ScanCallback() {}) + try { + scanCallback?.let { bleScanner.stopScan(it) } + } catch (e: Exception) { + Log.w(TAG, "Error stopping scan: ${e.message}") + } } - /** - * Handle scan result and connect to discovered devices - FIXED to prevent duplicate connections - */ - @Suppress("DEPRECATION") private fun handleScanResult(result: ScanResult) { val device = result.device val rssi = result.rssi val deviceAddress = device.address - // Filter out weak signals - if (rssi < -90) { + // Power-aware RSSI filtering + if (rssi < powerManager.getRSSIThreshold()) { return } - // FIXED: Check for existing client connections only, allow server connections - if (gattConnections.containsKey(device)) { - Log.d(TAG, "Already have client connection to $deviceAddress") + // Check if already connected or at connection limit + if (connectedDevices.containsKey(deviceAddress)) { return } - // FIXED: Check for pending connections to prevent duplicates + if (connectedDevices.size >= powerManager.getMaxConnections()) { + Log.d(TAG, "Connection limit reached (${powerManager.getMaxConnections()})") + return + } + + // Check connection attempts val currentTime = System.currentTimeMillis() - val pendingTime = pendingConnections[deviceAddress] - if (pendingTime != null && (currentTime - pendingTime) < connectionRetryDelay) { - Log.d(TAG, "Connection attempt to $deviceAddress already pending, skipping") - return - } + val existingAttempt = pendingConnections[deviceAddress] - // Check connection attempt limits - val attempts = connectionAttempts.getOrDefault(deviceAddress, 0) - if (attempts >= maxConnectionAttempts) { - if (pendingTime == null || (currentTime - pendingTime) > connectionRetryDelay * 2) { - // Reset after extended delay - connectionAttempts[deviceAddress] = 0 + if (existingAttempt != null) { + if (existingAttempt.isExpired()) { pendingConnections.remove(deviceAddress) - Log.d(TAG, "Reset connection attempts for $deviceAddress after extended delay") - } else { - Log.d(TAG, "Max connection attempts reached for $deviceAddress") + } else if (!existingAttempt.shouldRetry()) { return } } - // Store RSSI and track connection attempt - peripheralRSSI[deviceAddress] = rssi - pendingConnections[deviceAddress] = currentTime - connectionAttempts[deviceAddress] = attempts + 1 + // Update connection attempt + val attempts = (existingAttempt?.attempts ?: 0) + 1 + pendingConnections[deviceAddress] = ConnectionAttempt(attempts) - Log.i(TAG, "Found bitchat service at $deviceAddress (RSSI: $rssi), connecting (attempt ${attempts + 1})...") + Log.i(TAG, "Connecting to $deviceAddress (RSSI: $rssi, attempt: $attempts)") - // Connect to device - connectToDeviceFixed(device) + connectToDevice(device, rssi) } - /** - * Connect to a discovered device - FIXED version with proper timeout and cleanup handling - */ @Suppress("DEPRECATION") - private fun connectToDeviceFixed(device: BluetoothDevice) { + private fun connectToDevice(device: BluetoothDevice, rssi: Int) { if (!hasBluetoothPermissions()) return val deviceAddress = device.address - Log.d(TAG, "Attempting to connect to $deviceAddress") val gattCallback = object : BluetoothGattCallback() { override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { - val statusMessage = when (status) { - BluetoothGatt.GATT_SUCCESS -> "SUCCESS" - 147 -> "CONNECTION_TIMEOUT" - 133 -> "GATT_ERROR" - 8 -> "CONNECTION_TIMEOUT" - else -> "ERROR_$status" - } - - Log.d(TAG, "onConnectionStateChange: $deviceAddress status=$statusMessage($status) newState=$newState") - when (newState) { BluetoothProfile.STATE_CONNECTED -> { if (status == BluetoothGatt.GATT_SUCCESS) { - Log.i(TAG, "Successfully connected to $deviceAddress as client") - connectedDevices[deviceAddress] = gatt.device - gattConnections[gatt.device] = gatt - pendingConnections.remove(deviceAddress) // Clear pending connection + Log.i(TAG, "Client: Connected to $deviceAddress") + val deviceConn = DeviceConnection( + device = device, + gatt = gatt, + rssi = rssi, + isClient = true + ) + connectedDevices[deviceAddress] = deviceConn + pendingConnections.remove(deviceAddress) - // Discover services with a slight delay for stability connectionScope.launch { - delay(200) // Small delay for connection stabilization - if (gattConnections.containsKey(gatt.device)) { - gatt.discoverServices() - } + delay(200) + gatt.discoverServices() } } else { - Log.e(TAG, "Connected to $deviceAddress with error status: $status") gatt.disconnect() } } BluetoothProfile.STATE_DISCONNECTED -> { - Log.d(TAG, "Disconnected from $deviceAddress (status: $status)") - connectedDevices.remove(deviceAddress) - deviceCharacteristics.remove(gatt.device) - gattConnections.remove(gatt.device) - pendingConnections.remove(deviceAddress) // Clear pending connection + Log.d(TAG, "Client: Disconnected from $deviceAddress") + cleanupDeviceConnection(deviceAddress) - // Handle connection errors - if (status == 147 || status == 133 || status == 8) { // Timeout or connection errors - Log.w(TAG, "Connection failed to $deviceAddress with status $status") - } - - // Close the GATT connection - try { - gatt.close() - } catch (e: Exception) { - Log.e(TAG, "Error closing GATT connection to $deviceAddress: ${e.message}") + connectionScope.launch { + delay(CLEANUP_DELAY) + try { + gatt.close() + } catch (e: Exception) { + Log.w(TAG, "Error closing GATT: ${e.message}") + } } } } } override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { - Log.d(TAG, "onServicesDiscovered: $deviceAddress status=$status") - if (status == BluetoothGatt.GATT_SUCCESS) { val service = gatt.getService(SERVICE_UUID) val characteristic = service?.getCharacteristic(CHARACTERISTIC_UUID) if (characteristic != null) { - Log.d(TAG, "Found bitchat service and characteristic on $deviceAddress") - deviceCharacteristics[gatt.device] = characteristic + // Update device connection with characteristic + connectedDevices[deviceAddress]?.let { deviceConn -> + val updatedConn = deviceConn.copy(characteristic = characteristic) + connectedDevices[deviceAddress] = updatedConn + } // Enable notifications - val notificationSuccess = gatt.setCharacteristicNotification(characteristic, true) - Log.d(TAG, "Set notification for $deviceAddress: $notificationSuccess") + gatt.setCharacteristicNotification(characteristic, true) - // Write notification descriptor val descriptor = characteristic.getDescriptor( UUID.fromString("00002902-0000-1000-8000-00805f9b34fb") ) - if (descriptor != null) { - descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE - val descriptorSuccess = gatt.writeDescriptor(descriptor) - Log.d(TAG, "Write notification descriptor for $deviceAddress: $descriptorSuccess") + descriptor?.let { + it.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE + gatt.writeDescriptor(it) } - // Notify delegate about successful connection connectionScope.launch { - delay(100) // Ensure everything is set up - delegate?.onDeviceConnected(gatt.device) + delay(100) + delegate?.onDeviceConnected(device) } } else { - Log.e(TAG, "Bitchat service or characteristic not found on $deviceAddress") gatt.disconnect() } } else { - Log.e(TAG, "Service discovery failed on $deviceAddress with status: $status") gatt.disconnect() } } override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { val value = characteristic.value - Log.d(TAG, "Received ${value.size} bytes from $deviceAddress") - val packet = BitchatPacket.fromBinaryData(value) if (packet != null) { val peerID = String(packet.senderID).replace("\u0000", "") delegate?.onPacketReceived(packet, peerID, gatt.device) - } else { - Log.w(TAG, "Failed to parse packet from $deviceAddress") - } - } - - override fun onDescriptorWrite(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) { - Log.d(TAG, "onDescriptorWrite: $deviceAddress status=$status") - if (status == BluetoothGatt.GATT_SUCCESS) { - Log.i(TAG, "Successfully enabled notifications for $deviceAddress") - } else { - Log.e(TAG, "Failed to enable notifications for $deviceAddress: $status") } } } - // Attempt the connection try { - val gatt = device.connectGatt(context, false, gattCallback) - if (gatt == null) { - Log.e(TAG, "Failed to create GATT connection to $deviceAddress") - pendingConnections.remove(deviceAddress) - } + device.connectGatt(context, false, gattCallback) } catch (e: Exception) { Log.e(TAG, "Exception connecting to $deviceAddress: ${e.message}") pendingConnections.remove(deviceAddress) } } - /** - * Connect to a discovered device - LEGACY METHOD (kept for compatibility) - */ - @Suppress("DEPRECATION") - private fun connectToDevice(device: BluetoothDevice) { - // Redirect to fixed method - connectToDeviceFixed(device) + 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 } + } + pendingConnections.remove(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}") + } + } + } } - /** - * Clear all connection tracking - */ private fun clearAllConnections() { connectedDevices.clear() - deviceCharacteristics.clear() subscribedDevices.clear() - gattConnections.clear() - peripheralRSSI.clear() - // FIXED: Clear new tracking maps pendingConnections.clear() - connectionAttempts.clear() } } 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 b6a9a9ff..0c185686 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt @@ -44,7 +44,7 @@ class BluetoothMeshService(private val context: Context) { private val securityManager = SecurityManager(encryptionService, myPeerID) private val storeForwardManager = StoreForwardManager() private val messageHandler = MessageHandler(myPeerID) - private val connectionManager = BluetoothConnectionManager(context, myPeerID) + internal val connectionManager = BluetoothConnectionManager(context, myPeerID) // Made internal for access private val packetProcessor = PacketProcessor(myPeerID) // Delegate for message callbacks (maintains same interface) diff --git a/app/src/main/java/com/bitchat/android/mesh/PowerManager.kt b/app/src/main/java/com/bitchat/android/mesh/PowerManager.kt new file mode 100644 index 00000000..69358e23 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/mesh/PowerManager.kt @@ -0,0 +1,337 @@ +package com.bitchat.android.mesh + +import android.bluetooth.le.AdvertiseSettings +import android.bluetooth.le.ScanSettings +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.BatteryManager +import android.util.Log +import kotlinx.coroutines.* +import kotlin.math.max + +/** + * Power-aware Bluetooth management for bitchat + * Adjusts scanning, advertising, and connection behavior based on battery state + */ +class PowerManager(private val context: Context) { + + companion object { + private const val TAG = "PowerManager" + + // Battery thresholds + private const val CRITICAL_BATTERY = 10 + private const val LOW_BATTERY = 20 + private const val MEDIUM_BATTERY = 50 + + // Scan duty cycle periods (ms) + private const val SCAN_ON_DURATION_NORMAL = 2000L // 2 seconds on + private const val SCAN_OFF_DURATION_NORMAL = 8000L // 8 seconds off + private const val SCAN_ON_DURATION_POWER_SAVE = 1000L // 1 second on + private const val SCAN_OFF_DURATION_POWER_SAVE = 15000L // 15 seconds off + private const val SCAN_ON_DURATION_ULTRA_LOW = 500L // 0.5 seconds on + private const val SCAN_OFF_DURATION_ULTRA_LOW = 30000L // 30 seconds off + + // Connection limits + private const val MAX_CONNECTIONS_NORMAL = 8 + private const val MAX_CONNECTIONS_POWER_SAVE = 4 + private const val MAX_CONNECTIONS_ULTRA_LOW = 2 + } + + enum class PowerMode { + PERFORMANCE, // Full power, no restrictions + BALANCED, // Moderate power saving + POWER_SAVER, // Aggressive power saving + ULTRA_LOW_POWER // Minimal operations only + } + + private var currentMode = PowerMode.BALANCED + private var isCharging = false + private var batteryLevel = 100 + private var isAppInBackground = false + + private val powerScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + private var dutyCycleJob: Job? = null + + var delegate: PowerManagerDelegate? = null + + // Battery monitoring + private val batteryReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + when (intent?.action) { + Intent.ACTION_BATTERY_CHANGED -> { + val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) + val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1) + if (level != -1 && scale != -1) { + batteryLevel = (level * 100) / scale + } + + val status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1) + isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || + status == BatteryManager.BATTERY_STATUS_FULL + + updatePowerMode() + } + Intent.ACTION_POWER_CONNECTED -> { + isCharging = true + updatePowerMode() + } + Intent.ACTION_POWER_DISCONNECTED -> { + isCharging = false + updatePowerMode() + } + } + } + } + + init { + registerBatteryReceiver() + updatePowerMode() + } + + fun start() { + Log.i(TAG, "Starting power management") + startDutyCycle() + } + + fun stop() { + Log.i(TAG, "Stopping power management") + powerScope.cancel() + unregisterBatteryReceiver() + } + + fun setAppBackgroundState(inBackground: Boolean) { + if (isAppInBackground != inBackground) { + isAppInBackground = inBackground + Log.d(TAG, "App background state changed: $inBackground") + updatePowerMode() + } + } + + /** + * Get scan settings optimized for current power mode + */ + fun getScanSettings(): ScanSettings { + return when (currentMode) { + PowerMode.PERFORMANCE -> ScanSettings.Builder() + .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) + .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) + .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE) + .setNumOfMatches(ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT) + .setReportDelay(50) // Reduced from 10ms + .build() + + PowerMode.BALANCED -> ScanSettings.Builder() + .setScanMode(ScanSettings.SCAN_MODE_BALANCED) + .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) + .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE) + .setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT) + .setReportDelay(200) + .build() + + PowerMode.POWER_SAVER -> ScanSettings.Builder() + .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER) + .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) + .setMatchMode(ScanSettings.MATCH_MODE_STICKY) + .setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT) + .setReportDelay(1000) + .build() + + PowerMode.ULTRA_LOW_POWER -> ScanSettings.Builder() + .setScanMode(ScanSettings.SCAN_MODE_OPPORTUNISTIC) + .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) + .setMatchMode(ScanSettings.MATCH_MODE_STICKY) + .setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT) + .setReportDelay(5000) + .build() + } + } + + /** + * Get advertising settings optimized for current power mode + */ + fun getAdvertiseSettings(): AdvertiseSettings { + return when (currentMode) { + PowerMode.PERFORMANCE -> AdvertiseSettings.Builder() + .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY) + .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH) + .setConnectable(true) + .setTimeout(0) + .build() + + PowerMode.BALANCED -> AdvertiseSettings.Builder() + .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED) + .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM) + .setConnectable(true) + .setTimeout(0) + .build() + + PowerMode.POWER_SAVER -> AdvertiseSettings.Builder() + .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_POWER) + .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_LOW) + .setConnectable(true) + .setTimeout(0) + .build() + + PowerMode.ULTRA_LOW_POWER -> AdvertiseSettings.Builder() + .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_POWER) + .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_ULTRA_LOW) + .setConnectable(true) + .setTimeout(0) + .build() + } + } + + /** + * Get maximum allowed connections for current power mode + */ + fun getMaxConnections(): Int { + return when (currentMode) { + PowerMode.PERFORMANCE -> MAX_CONNECTIONS_NORMAL + PowerMode.BALANCED -> MAX_CONNECTIONS_NORMAL + PowerMode.POWER_SAVER -> MAX_CONNECTIONS_POWER_SAVE + PowerMode.ULTRA_LOW_POWER -> MAX_CONNECTIONS_ULTRA_LOW + } + } + + /** + * Get RSSI filter threshold for current power mode + */ + fun getRSSIThreshold(): Int { + return when (currentMode) { + PowerMode.PERFORMANCE -> -95 + PowerMode.BALANCED -> -85 + PowerMode.POWER_SAVER -> -75 + PowerMode.ULTRA_LOW_POWER -> -65 + } + } + + /** + * Check if duty cycling should be used + */ + fun shouldUseDutyCycle(): Boolean { + return currentMode != PowerMode.PERFORMANCE + } + + /** + * Get current power mode information + */ + fun getPowerInfo(): String { + return buildString { + appendLine("=== Power Manager Status ===") + appendLine("Current Mode: $currentMode") + appendLine("Battery Level: $batteryLevel%") + appendLine("Is Charging: $isCharging") + appendLine("App In Background: $isAppInBackground") + appendLine("Max Connections: ${getMaxConnections()}") + appendLine("RSSI Threshold: ${getRSSIThreshold()} dBm") + appendLine("Use Duty Cycle: ${shouldUseDutyCycle()}") + } + } + + private fun updatePowerMode() { + val newMode = when { + // Always use performance mode when charging (unless in background too long) + isCharging && !isAppInBackground -> PowerMode.PERFORMANCE + + // Critical battery - use ultra low power + batteryLevel <= CRITICAL_BATTERY -> PowerMode.ULTRA_LOW_POWER + + // Low battery - use power saver + batteryLevel <= LOW_BATTERY -> PowerMode.POWER_SAVER + + // Background app with medium battery - use power saver + isAppInBackground && batteryLevel <= MEDIUM_BATTERY -> PowerMode.POWER_SAVER + + // Background app with good battery - use balanced + isAppInBackground -> PowerMode.BALANCED + + // Foreground with good battery - use balanced + else -> PowerMode.BALANCED + } + + if (newMode != currentMode) { + val oldMode = currentMode + currentMode = newMode + Log.i(TAG, "Power mode changed: $oldMode → $newMode (battery: $batteryLevel%, charging: $isCharging, background: $isAppInBackground)") + + delegate?.onPowerModeChanged(currentMode) + + // Restart duty cycle with new parameters + if (shouldUseDutyCycle()) { + startDutyCycle() + } else { + stopDutyCycle() + } + } + } + + private fun startDutyCycle() { + stopDutyCycle() + + if (!shouldUseDutyCycle()) { + delegate?.onScanStateChanged(true) // Always scan in performance mode + return + } + + val (onDuration, offDuration) = when (currentMode) { + PowerMode.BALANCED -> SCAN_ON_DURATION_NORMAL to SCAN_OFF_DURATION_NORMAL + PowerMode.POWER_SAVER -> SCAN_ON_DURATION_POWER_SAVE to SCAN_OFF_DURATION_POWER_SAVE + PowerMode.ULTRA_LOW_POWER -> SCAN_ON_DURATION_ULTRA_LOW to SCAN_OFF_DURATION_ULTRA_LOW + PowerMode.PERFORMANCE -> return // No duty cycle + } + + dutyCycleJob = powerScope.launch { + while (isActive && shouldUseDutyCycle()) { + // Scan ON period + Log.d(TAG, "Duty cycle: Scan ON for ${onDuration}ms") + delegate?.onScanStateChanged(true) + delay(onDuration) + + // Scan OFF period (keep advertising active) + if (isActive && shouldUseDutyCycle()) { + Log.d(TAG, "Duty cycle: Scan OFF for ${offDuration}ms") + delegate?.onScanStateChanged(false) + delay(offDuration) + } + } + } + + Log.i(TAG, "Started duty cycle: ${onDuration}ms ON, ${offDuration}ms OFF") + } + + private fun stopDutyCycle() { + dutyCycleJob?.cancel() + dutyCycleJob = null + } + + private fun registerBatteryReceiver() { + try { + val filter = IntentFilter().apply { + addAction(Intent.ACTION_BATTERY_CHANGED) + addAction(Intent.ACTION_POWER_CONNECTED) + addAction(Intent.ACTION_POWER_DISCONNECTED) + } + context.registerReceiver(batteryReceiver, filter) + } catch (e: Exception) { + Log.w(TAG, "Failed to register battery receiver: ${e.message}") + } + } + + private fun unregisterBatteryReceiver() { + try { + context.unregisterReceiver(batteryReceiver) + } catch (e: Exception) { + Log.w(TAG, "Failed to unregister battery receiver: ${e.message}") + } + } +} + +/** + * Delegate interface for power management callbacks + */ +interface PowerManagerDelegate { + fun onPowerModeChanged(newMode: PowerManager.PowerMode) + fun onScanStateChanged(shouldScan: Boolean) +} diff --git a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt index d4db8301..4cfb92c3 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt @@ -259,6 +259,11 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B } } + fun setAppBackgroundState(inBackground: Boolean) { + // Forward to connection manager for power optimization + meshService.connectionManager.setAppBackgroundState(inBackground) + } + // MARK: - Command Autocomplete (delegated) fun updateCommandSuggestions(input: String) { From 1d7defcbae80ea84f44710da3362a5aa82a52efb Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 9 Jul 2025 18:29:49 +0200 Subject: [PATCH 4/8] rate limiting --- .../mesh/BluetoothConnectionManager.kt | 295 ++++++++++++++---- 1 file changed, 237 insertions(+), 58 deletions(-) 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 efa8dada..84ec62cf 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt @@ -62,6 +62,12 @@ class BluetoothConnectionManager( 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 @@ -140,7 +146,7 @@ class BluetoothConnectionManager( startPeriodicCleanup() - Log.i(TAG, "Power-optimized Bluetooth services started successfully") + Log.i(TAG, "Power-optimized Bluetooth services started successfully (CLIENT ONLY)") } return true @@ -276,16 +282,30 @@ class BluetoothConnectionManager( Log.i(TAG, "Power mode changed to: $newMode") connectionScope.launch { - // Update advertising and scanning based on new power mode + // CRITICAL FIX: Avoid rapid scan restarts by checking if we need to change scan behavior + val wasUsingDutyCycle = powerManager.shouldUseDutyCycle() + + // Update advertising with new power settings stopAdvertising() delay(100) startAdvertising() - // Restart scanning with new settings if not using duty cycle - if (!powerManager.shouldUseDutyCycle()) { + // 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(100) - startScanning() + 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() + } + } else { + Log.d(TAG, "Duty cycle behavior unchanged, keeping existing scan state") } // Enforce connection limits @@ -431,7 +451,7 @@ class BluetoothConnectionManager( @Suppress("DEPRECATION") private fun startAdvertising() { if (!hasBluetoothPermissions() || bleAdvertiser == null || !isActive) return - + val settings = powerManager.getAdvertiseSettings() val data = AdvertiseData.Builder() @@ -471,38 +491,111 @@ class BluetoothConnectionManager( 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() + // DEBUG: For now, let's scan ALL devices to see what's around + // Change back to listOf(scanFilter) when we understand the issue + val scanFilters = emptyList() // No filter = see all devices + + Log.d(TAG, "Starting BLE scan with ${scanFilters.size} filters (0=all devices). 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 + val scanRecord = result.scanRecord + Log.d(TAG, "Scan result: device: ${device.address}, Name: '${device.name}', RSSI: $rssi") handleScanResult(result) } override fun onBatchScanResults(results: MutableList) { - results.forEach { handleScanResult(it) } + Log.d(TAG, "Batch scan results received: ${results.size} devices") + results.forEach { result -> + Log.d(TAG, "Batch result: ${result.device.address} (${result.device.name}) RSSI: ${result.rssi}") + 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 { - bleScanner.startScan(listOf(scanFilter), powerManager.getScanSettings(), scanCallback) + 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 - try { - scanCallback?.let { bleScanner.stopScan(it) } - } catch (e: Exception) { - Log.w(TAG, "Error stopping scan: ${e.message}") + + 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() } } @@ -510,41 +603,75 @@ class BluetoothConnectionManager( val device = result.device val rssi = result.rssi val deviceAddress = device.address + val scanRecord = result.scanRecord - // Power-aware RSSI filtering - if (rssi < powerManager.getRSSIThreshold()) { + // CRITICAL: Only process devices that have our service UUID + val hasOurService = scanRecord?.serviceUuids?.any { it.uuid == SERVICE_UUID } == true + if (!hasOurService) { + // Still log for debugging but don't try to connect + Log.d(TAG, "Skipping device $deviceAddress - doesn't advertise our service UUID") return } - // Check if already connected or at connection limit - if (connectedDevices.containsKey(deviceAddress)) { + // 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 + } + + Log.d(TAG, "Processing bitchat device: $deviceAddress, name: '$deviceName', peerID: $extractedPeerID, RSSI: $rssi") + + // 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 } - // Check connection attempts - val currentTime = System.currentTimeMillis() - val existingAttempt = pendingConnections[deviceAddress] - - if (existingAttempt != null) { - if (existingAttempt.isExpired()) { - pendingConnections.remove(deviceAddress) - } else if (!existingAttempt.shouldRetry()) { + // 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) + + if (extractedPeerID != null) { + Log.i(TAG, "Initiating connection to peer $extractedPeerID at $deviceAddress (RSSI: $rssi, attempt: $attempts)") + } else { + Log.i(TAG, "Initiating connection to device with bitchat service at $deviceAddress (RSSI: $rssi, attempt: $attempts)") + } + + // Start connection immediately while holding lock + connectToDevice(device, rssi) } - - // Update connection attempt - val attempts = (existingAttempt?.attempts ?: 0) + 1 - pendingConnections[deviceAddress] = ConnectionAttempt(attempts) - - Log.i(TAG, "Connecting to $deviceAddress (RSSI: $rssi, attempt: $attempts)") - - connectToDevice(device, rssi) } @Suppress("DEPRECATION") @@ -555,10 +682,13 @@ class BluetoothConnectionManager( val gattCallback = object : BluetoothGattCallback() { override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { + // FIXED: Enhanced error logging to diagnose connection failures + Log.d(TAG, "Client: Connection state change - Device: $deviceAddress, Status: $status, NewState: $newState") + when (newState) { BluetoothProfile.STATE_CONNECTED -> { if (status == BluetoothGatt.GATT_SUCCESS) { - Log.i(TAG, "Client: Connected to $deviceAddress") + Log.i(TAG, "Client: Successfully connected to $deviceAddress") val deviceConn = DeviceConnection( device = device, gatt = gatt, @@ -569,15 +699,34 @@ class BluetoothConnectionManager( pendingConnections.remove(deviceAddress) connectionScope.launch { - delay(200) + delay(500) // FIXED: Increased delay for better stability + Log.d(TAG, "Starting service discovery for $deviceAddress") gatt.discoverServices() } } else { + Log.w(TAG, "Client: Connection failed to $deviceAddress with status $status") + // CRITICAL FIX: Clean up failed connection attempt immediately + pendingConnections.remove(deviceAddress) gatt.disconnect() } } + BluetoothProfile.STATE_CONNECTING -> { + Log.d(TAG, "Client: Connecting to $deviceAddress...") + } + BluetoothProfile.STATE_DISCONNECTING -> { + Log.d(TAG, "Client: Disconnecting from $deviceAddress...") + } BluetoothProfile.STATE_DISCONNECTED -> { - Log.d(TAG, "Client: Disconnected from $deviceAddress") + if (status != BluetoothGatt.GATT_SUCCESS) { + Log.w(TAG, "Client: Disconnected from $deviceAddress with error status $status") + // Special handling for connection establishment failure (status 147) + if (status == 147) { + Log.e(TAG, "Client: Connection establishment failed (status 147) for $deviceAddress - likely due to device compatibility or timing issues") + } + } else { + Log.d(TAG, "Client: Cleanly disconnected from $deviceAddress") + } + cleanupDeviceConnection(deviceAddress) connectionScope.launch { @@ -593,36 +742,56 @@ class BluetoothConnectionManager( } override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { + Log.d(TAG, "Client: Service discovery completed for $deviceAddress with status: $status") + if (status == BluetoothGatt.GATT_SUCCESS) { val service = gatt.getService(SERVICE_UUID) - val characteristic = service?.getCharacteristic(CHARACTERISTIC_UUID) + Log.d(TAG, "Client: Service found: ${service != null} for $deviceAddress") - if (characteristic != null) { - // Update device connection with characteristic - connectedDevices[deviceAddress]?.let { deviceConn -> - val updatedConn = deviceConn.copy(characteristic = characteristic) - connectedDevices[deviceAddress] = updatedConn - } + if (service != null) { + val characteristic = service.getCharacteristic(CHARACTERISTIC_UUID) + Log.d(TAG, "Client: Characteristic found: ${characteristic != null} for $deviceAddress") - // Enable notifications - gatt.setCharacteristicNotification(characteristic, true) - - val descriptor = characteristic.getDescriptor( - UUID.fromString("00002902-0000-1000-8000-00805f9b34fb") - ) - descriptor?.let { - it.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE - gatt.writeDescriptor(it) - } - - connectionScope.launch { - delay(100) - delegate?.onDeviceConnected(device) + if (characteristic != null) { + // Update device connection with characteristic + connectedDevices[deviceAddress]?.let { deviceConn -> + val updatedConn = deviceConn.copy(characteristic = characteristic) + connectedDevices[deviceAddress] = updatedConn + Log.d(TAG, "Client: Updated device connection with characteristic for $deviceAddress") + } + + // Enable notifications + val notificationEnabled = gatt.setCharacteristicNotification(characteristic, true) + Log.d(TAG, "Client: Notification enabled: $notificationEnabled for $deviceAddress") + + val descriptor = characteristic.getDescriptor( + UUID.fromString("00002902-0000-1000-8000-00805f9b34fb") + ) + + if (descriptor != null) { + descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE + val descriptorWritten = gatt.writeDescriptor(descriptor) + Log.d(TAG, "Client: Descriptor write initiated: $descriptorWritten for $deviceAddress") + + connectionScope.launch { + delay(200) // Wait for descriptor write to complete + 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() } } @@ -638,7 +807,14 @@ class BluetoothConnectionManager( } try { - device.connectGatt(context, false, gattCallback) + Log.d(TAG, "Attempting GATT connection to $deviceAddress with autoConnect=false") + val gatt = device.connectGatt(context, false, gattCallback) + if (gatt == null) { + Log.e(TAG, "connectGatt returned null for $deviceAddress") + pendingConnections.remove(deviceAddress) + } else { + Log.d(TAG, "GATT connection initiated successfully for $deviceAddress") + } } catch (e: Exception) { Log.e(TAG, "Exception connecting to $deviceAddress: ${e.message}") pendingConnections.remove(deviceAddress) @@ -694,7 +870,10 @@ class BluetoothConnectionManager( connectedDevices.remove(deviceAddress)?.let { deviceConn -> subscribedDevices.removeAll { it.address == 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() { From 250772a693b95f094bbfef937aff5fdb23a32251 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 9 Jul 2025 19:49:21 +0200 Subject: [PATCH 5/8] silent --- .../bitchat/android/mesh/BluetoothConnectionManager.kt | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) 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 84ec62cf..4a953176 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt @@ -518,11 +518,10 @@ class BluetoothConnectionManager( .setServiceUuid(ParcelUuid(SERVICE_UUID)) .build() - // DEBUG: For now, let's scan ALL devices to see what's around - // Change back to listOf(scanFilter) when we understand the issue - val scanFilters = emptyList() // No filter = see all devices + + val scanFilters = listOf(scanFilter) - Log.d(TAG, "Starting BLE scan with ${scanFilters.size} filters (0=all devices). Target service UUID: $SERVICE_UUID") + Log.d(TAG, "Starting BLE scan with target service UUID: $SERVICE_UUID") scanCallback = object : ScanCallback() { override fun onScanResult(callbackType: Int, result: ScanResult) { @@ -537,7 +536,6 @@ class BluetoothConnectionManager( override fun onBatchScanResults(results: MutableList) { Log.d(TAG, "Batch scan results received: ${results.size} devices") results.forEach { result -> - Log.d(TAG, "Batch result: ${result.device.address} (${result.device.name}) RSSI: ${result.rssi}") handleScanResult(result) } } @@ -608,8 +606,6 @@ class BluetoothConnectionManager( // CRITICAL: Only process devices that have our service UUID val hasOurService = scanRecord?.serviceUuids?.any { it.uuid == SERVICE_UUID } == true if (!hasOurService) { - // Still log for debugging but don't try to connect - Log.d(TAG, "Skipping device $deviceAddress - doesn't advertise our service UUID") return } From 90750c263566babde6ecdb2b0a6329dc350fa5fe Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 9 Jul 2025 21:10:20 +0200 Subject: [PATCH 6/8] disable server, but client works --- .../mesh/BluetoothConnectionManager.kt | 4 +- .../com/bitchat/android/mesh/PowerManager.kt | 63 +++++++++---------- 2 files changed, 32 insertions(+), 35 deletions(-) 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 4a953176..eea7d52f 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt @@ -128,7 +128,7 @@ class BluetoothConnectionManager( try { isActive = true - setupGattServer() + // setupGattServer() // Start power manager and services connectionScope.launch { @@ -804,7 +804,7 @@ class BluetoothConnectionManager( try { Log.d(TAG, "Attempting GATT connection to $deviceAddress with autoConnect=false") - val gatt = device.connectGatt(context, false, gattCallback) + val gatt = device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE) if (gatt == null) { Log.e(TAG, "connectGatt returned null for $deviceAddress") pendingConnections.remove(deviceAddress) diff --git a/app/src/main/java/com/bitchat/android/mesh/PowerManager.kt b/app/src/main/java/com/bitchat/android/mesh/PowerManager.kt index 69358e23..2c8bcab0 100644 --- a/app/src/main/java/com/bitchat/android/mesh/PowerManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/PowerManager.kt @@ -112,41 +112,38 @@ class PowerManager(private val context: Context) { /** * Get scan settings optimized for current power mode */ - fun getScanSettings(): ScanSettings { - return when (currentMode) { - PowerMode.PERFORMANCE -> ScanSettings.Builder() - .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) + fun getScanSettings(): ScanSettings { + // CRITICAL FIX: Set reportDelay to 0 for all modes. + // When using a custom duty cycle, we want scan results delivered immediately, + // not batched. A non-zero report delay can conflict with the scan window, + // causing missed results if the scan stops before the delay is met. + val builder = ScanSettings.Builder() .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) - .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE) - .setNumOfMatches(ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT) - .setReportDelay(50) // Reduced from 10ms - .build() - - PowerMode.BALANCED -> ScanSettings.Builder() - .setScanMode(ScanSettings.SCAN_MODE_BALANCED) - .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) - .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE) - .setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT) - .setReportDelay(200) - .build() - - PowerMode.POWER_SAVER -> ScanSettings.Builder() - .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER) - .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) - .setMatchMode(ScanSettings.MATCH_MODE_STICKY) - .setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT) - .setReportDelay(1000) - .build() - - PowerMode.ULTRA_LOW_POWER -> ScanSettings.Builder() - .setScanMode(ScanSettings.SCAN_MODE_OPPORTUNISTIC) - .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) - .setMatchMode(ScanSettings.MATCH_MODE_STICKY) - .setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT) - .setReportDelay(5000) - .build() + + when (currentMode) { + PowerMode.PERFORMANCE -> builder + .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) + .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE) + .setNumOfMatches(ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT) + + PowerMode.BALANCED -> builder + .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) + .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE) + .setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT) + + PowerMode.POWER_SAVER -> builder + .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER) + .setMatchMode(ScanSettings.MATCH_MODE_STICKY) + .setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT) + + PowerMode.ULTRA_LOW_POWER -> builder + .setScanMode(ScanSettings.SCAN_MODE_OPPORTUNISTIC) + .setMatchMode(ScanSettings.MATCH_MODE_STICKY) + .setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT) + } + + return builder.setReportDelay(0).build() } - } /** * Get advertising settings optimized for current power mode From 3c0d1454067a2215bffeb3629ccea1d22a0152af Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 9 Jul 2025 21:39:12 +0200 Subject: [PATCH 7/8] variable mtu clientside fix --- .../mesh/BluetoothConnectionManager.kt | 124 ++++++++---------- 1 file changed, 56 insertions(+), 68 deletions(-) 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 eea7d52f..3acd04d6 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt @@ -676,101 +676,89 @@ class BluetoothConnectionManager( val deviceAddress = device.address - val gattCallback = object : BluetoothGattCallback() { + val gattCallback = object : BluetoothGattCallback() { override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { - // FIXED: Enhanced error logging to diagnose connection failures Log.d(TAG, "Client: Connection state change - Device: $deviceAddress, Status: $status, NewState: $newState") - - when (newState) { - BluetoothProfile.STATE_CONNECTED -> { - if (status == BluetoothGatt.GATT_SUCCESS) { - Log.i(TAG, "Client: Successfully connected to $deviceAddress") - val deviceConn = DeviceConnection( - device = device, - gatt = gatt, - rssi = rssi, - isClient = true - ) - connectedDevices[deviceAddress] = deviceConn - pendingConnections.remove(deviceAddress) - - connectionScope.launch { - delay(500) // FIXED: Increased delay for better stability - Log.d(TAG, "Starting service discovery for $deviceAddress") - gatt.discoverServices() - } - } else { - Log.w(TAG, "Client: Connection failed to $deviceAddress with status $status") - // CRITICAL FIX: Clean up failed connection attempt immediately - pendingConnections.remove(deviceAddress) - gatt.disconnect() + + 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") } - BluetoothProfile.STATE_CONNECTING -> { - Log.d(TAG, "Client: Connecting to $deviceAddress...") - } - BluetoothProfile.STATE_DISCONNECTING -> { - Log.d(TAG, "Client: Disconnecting from $deviceAddress...") - } - BluetoothProfile.STATE_DISCONNECTED -> { - if (status != BluetoothGatt.GATT_SUCCESS) { - Log.w(TAG, "Client: Disconnected from $deviceAddress with error status $status") - // Special handling for connection establishment failure (status 147) - if (status == 147) { - Log.e(TAG, "Client: Connection establishment failed (status 147) for $deviceAddress - likely due to device compatibility or timing issues") - } - } 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}") - } + + 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) { Log.d(TAG, "Client: Service discovery completed for $deviceAddress with status: $status") if (status == BluetoothGatt.GATT_SUCCESS) { val service = gatt.getService(SERVICE_UUID) - Log.d(TAG, "Client: Service found: ${service != null} for $deviceAddress") - if (service != null) { val characteristic = service.getCharacteristic(CHARACTERISTIC_UUID) - Log.d(TAG, "Client: Characteristic found: ${characteristic != null} for $deviceAddress") - if (characteristic != null) { - // Update device connection with characteristic connectedDevices[deviceAddress]?.let { deviceConn -> val updatedConn = deviceConn.copy(characteristic = characteristic) connectedDevices[deviceAddress] = updatedConn Log.d(TAG, "Client: Updated device connection with characteristic for $deviceAddress") } - // Enable notifications - val notificationEnabled = gatt.setCharacteristicNotification(characteristic, true) - Log.d(TAG, "Client: Notification enabled: $notificationEnabled for $deviceAddress") - - val descriptor = characteristic.getDescriptor( - UUID.fromString("00002902-0000-1000-8000-00805f9b34fb") - ) - + gatt.setCharacteristicNotification(characteristic, true) + val descriptor = characteristic.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")) if (descriptor != null) { descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE - val descriptorWritten = gatt.writeDescriptor(descriptor) - Log.d(TAG, "Client: Descriptor write initiated: $descriptorWritten for $deviceAddress") + gatt.writeDescriptor(descriptor) connectionScope.launch { - delay(200) // Wait for descriptor write to complete + delay(200) Log.i(TAG, "Client: Connection setup complete for $deviceAddress") delegate?.onDeviceConnected(device) } From b6f281275a4f3fdbd40ffab72881c1a7894bab6a Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 9 Jul 2025 21:51:04 +0200 Subject: [PATCH 8/8] fixed --- .../com/bitchat/android/mesh/BluetoothConnectionManager.kt | 2 +- .../java/com/bitchat/android/mesh/BluetoothMeshService.kt | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) 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 3acd04d6..faaa3a45 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt @@ -128,7 +128,7 @@ class BluetoothConnectionManager( try { isActive = true - // setupGattServer() + setupGattServer() // Start power manager and services connectionScope.launch { 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 0c185686..0a66bcf8 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt @@ -350,10 +350,7 @@ class BluetoothMeshService(private val context: Context) { ) // Send with random delay and retry for reliability - delay(Random.nextLong(50, 500)) - connectionManager.broadcastPacket(packet) - - delay(300 + Random.nextLong(0, 200)) + // delay(Random.nextLong(50, 500)) connectionManager.broadcastPacket(packet) } }