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 com.bitchat.android.util.AppConstants import com.bitchat.android.util.PeerId 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?, private val myPeerID: String ) { companion object { private const val TAG = "BluetoothGattServerManager" } // 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 /** * Disconnect a specific device (used by ConnectionManager to enforce overall limits) */ fun disconnectDevice(device: BluetoothDevice) { try { gattServer?.cancelConnection(device) } catch (e: Exception) { Log.w(TAG, "Error disconnecting device ${device.address}: ${e.message}") } } /** * Start GATT server */ fun start(): Boolean { // Respect debug setting try { if (!com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().gattServerEnabled.value) { Log.i(TAG, "Server start skipped: GATT Server disabled in debug settings") return false } } catch (_: Exception) { } if (isActive) { Log.d(TAG, "GATT server already active; start is a no-op") return true } 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() { if (!isActive) { // Idempotent stop stopAdvertising() // Ensure server is closed if present gattServer?.close() gattServer = null Log.i(TAG, "GATT server stopped (already inactive)") return } isActive = false connectionScope.launch { stopAdvertising() // Try to cancel any active connections explicitly before closing try { // Disconnect ALL server connections val servers = connectionTracker.getConnectedDevices().values.filter { !it.isClient } servers.forEach { d -> try { gattServer?.cancelConnection(d.device) } catch (_: Exception) { } } } catch (_: Exception) { } // 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}") // Get best available RSSI (scan RSSI for server connections) val rssi = connectionTracker.getBestRSSI(device.address) ?: Int.MIN_VALUE val deviceConn = BluetoothConnectionTracker.DeviceConnection( device = device, rssi = rssi, isClient = false ) connectionTracker.addDeviceConnection(device.address, deviceConn) connectionScope.launch { delay(1000) if (isActive) { // Check if still active delegate?.onDeviceConnected(device) } } } BluetoothProfile.STATE_DISCONNECTED -> { Log.i(TAG, "Server: Device disconnected ${device.address}") connectionTracker.cleanupDeviceConnection(device.address) // Notify delegate about device disconnection so higher layers can update direct flags delegate?.onDeviceDisconnected(device) } } } 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 == AppConstants.Mesh.Gatt.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 = PeerId.fromBytes(packet.senderID) 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)) { connectionTracker.addSubscribedDevice(device) Log.d(TAG, "Server: Connection setup complete for ${device.address}") 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( AppConstants.Mesh.Gatt.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( AppConstants.Mesh.Gatt.DESCRIPTOR_UUID, BluetoothGattDescriptor.PERMISSION_READ or BluetoothGattDescriptor.PERMISSION_WRITE ) characteristic?.addDescriptor(descriptor) val service = BluetoothGattService(AppConstants.Mesh.Gatt.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() { // Respect debug setting val enabled = try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().gattServerEnabled.value } catch (_: Exception) { true } // Guard conditions – never throw here to avoid crashing the app from a background coroutine if (!permissionManager.hasBluetoothPermissions()) { Log.w(TAG, "Not starting advertising: missing Bluetooth permissions") return } if (bluetoothAdapter == null) { Log.w(TAG, "Not starting advertising: bluetoothAdapter is null") return } if (!isActive) { Log.d(TAG, "Not starting advertising: manager not active") return } if (!enabled) { Log.i(TAG, "Not starting advertising: GATT Server disabled via debug settings") return } if (bleAdvertiser == null) { Log.w(TAG, "Not starting advertising: BLE advertiser not available on this device") return } if (!bluetoothAdapter.isMultipleAdvertisementSupported) { Log.w(TAG, "Not starting advertising: multiple advertisement not supported on this device") return } val settings = powerManager.getAdvertiseSettings() val data = AdvertiseData.Builder() .addServiceUuid(ParcelUuid(AppConstants.Mesh.Gatt.SERVICE_UUID)) .setIncludeTxPowerLevel(false) .setIncludeDeviceName(false) .build() // Add stable identity (first 8 bytes of peerID) to Scan Response // This allows scanners to deduplicate devices even if MAC address rotates val peerIDBytes = try { myPeerID.chunked(2).map { it.toInt(16).toByte() }.toByteArray().take(8).toByteArray() } catch (e: Exception) { ByteArray(0) } val scanResponse = AdvertiseData.Builder() .addServiceData(ParcelUuid(AppConstants.Mesh.Gatt.SERVICE_UUID), peerIDBytes) .setIncludeTxPowerLevel(false) .setIncludeDeviceName(false) .build() advertiseCallback = object : AdvertiseCallback() { override fun onStartSuccess(settingsInEffect: AdvertiseSettings) { val mode = try { powerManager.getPowerInfo().split("Current Mode: ")[1].split("\n")[0] } catch (_: Exception) { "unknown" } Log.i(TAG, "Advertising started (power mode: $mode) with stable ID: ${PeerId.fromBytes(peerIDBytes)}") } override fun onStartFailure(errorCode: Int) { Log.e(TAG, "Advertising failed: $errorCode") } } try { bleAdvertiser.startAdvertising(settings, data, scanResponse, advertiseCallback) } catch (se: SecurityException) { Log.e(TAG, "SecurityException starting advertising (missing permission?): ${se.message}") } 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 { cb -> bleAdvertiser.stopAdvertising(cb) } } catch (e: Exception) { Log.w(TAG, "Error stopping advertising: ${e.message}") } } /** * Restart advertising (for power mode changes) */ fun restartAdvertising() { // Respect debug setting val enabled = try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().gattServerEnabled.value } catch (_: Exception) { true } if (!isActive || !enabled) { stopAdvertising() return } connectionScope.launch { stopAdvertising() delay(100) startAdvertising() } } }