Files
bitchat-android/app/src/main/java/com/bitchat/android/mesh/BluetoothGattServerManager.kt
T
2025-09-14 03:50:51 +02:00

418 lines
16 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.bitchat.android.mesh
import android.bluetooth.*
import android.bluetooth.le.AdvertiseCallback
import android.bluetooth.le.AdvertiseData
import android.bluetooth.le.AdvertiseSettings
import android.bluetooth.le.BluetoothLeAdvertiser
import android.content.Context
import android.os.ParcelUuid
import android.util.Log
import com.bitchat.android.protocol.BitchatPacket
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.util.*
/**
* Manages GATT server operations, advertising, and server-side connections
*/
class BluetoothGattServerManager(
private val context: Context,
private val connectionScope: CoroutineScope,
private val connectionTracker: BluetoothConnectionTracker,
private val permissionManager: BluetoothPermissionManager,
private val powerManager: PowerManager,
private val delegate: BluetoothConnectionManagerDelegate?
) {
companion object {
private const val TAG = "BluetoothGattServerManager"
// Use exact same UUIDs as iOS version
private val SERVICE_UUID = UUID.fromString("F47B5E2D-4A9E-4C5A-9B3F-8E1D2C3A4B5C")
private val CHARACTERISTIC_UUID = UUID.fromString("A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5D")
private val DESCRIPTOR_UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
}
// 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
// Enforce a server connection limit by canceling the oldest connections (best-effort)
fun enforceServerLimit(maxServer: Int) {
if (maxServer <= 0) return
try {
val subs = connectionTracker.getSubscribedDevices()
if (subs.size > maxServer) {
val excess = subs.size - maxServer
subs.take(excess).forEach { d ->
try { gattServer?.cancelConnection(d) } catch (_: Exception) { }
}
}
} catch (_: Exception) { }
}
/**
* 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 {
val devices = connectionTracker.getSubscribedDevices()
devices.forEach { d ->
try { gattServer?.cancelConnection(d) } 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 == 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 = packet.senderID.take(8).toByteArray().joinToString("") { "%02x".format(it) }
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(
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(
DESCRIPTOR_UUID,
BluetoothGattDescriptor.PERMISSION_READ or BluetoothGattDescriptor.PERMISSION_WRITE
)
characteristic?.addDescriptor(descriptor)
val service = BluetoothGattService(SERVICE_UUID, BluetoothGattService.SERVICE_TYPE_PRIMARY)
service.addCharacteristic(characteristic)
gattServer?.addService(service)
Log.i(TAG, "GATT server setup complete")
}
/**
* Start advertising
*/
@Suppress("DEPRECATION")
private fun startAdvertising() {
// 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(SERVICE_UUID))
.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)")
}
override fun onStartFailure(errorCode: Int) {
Log.e(TAG, "Advertising failed: $errorCode")
}
}
try {
bleAdvertiser.startAdvertising(settings, data, 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()
}
}
}