mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 22:25:26 +00:00
wip refactor
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,305 @@
|
|||||||
|
package com.bitchat.android.mesh
|
||||||
|
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import android.bluetooth.BluetoothGatt
|
||||||
|
import android.bluetooth.BluetoothGattCharacteristic
|
||||||
|
import android.util.Log
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracks all Bluetooth connections and handles cleanup
|
||||||
|
*/
|
||||||
|
class BluetoothConnectionTracker(
|
||||||
|
private val connectionScope: CoroutineScope,
|
||||||
|
private val powerManager: PowerManager
|
||||||
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "BluetoothConnectionTracker"
|
||||||
|
private const val CONNECTION_RETRY_DELAY = 5000L
|
||||||
|
private const val MAX_CONNECTION_ATTEMPTS = 3
|
||||||
|
private const val CLEANUP_DELAY = 500L
|
||||||
|
private const val CLEANUP_INTERVAL = 30000L // 30 seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connection tracking - reduced memory footprint
|
||||||
|
private val connectedDevices = ConcurrentHashMap<String, DeviceConnection>()
|
||||||
|
private val subscribedDevices = CopyOnWriteArrayList<BluetoothDevice>()
|
||||||
|
val addressPeerMap = ConcurrentHashMap<String, String>()
|
||||||
|
|
||||||
|
// Connection attempt tracking with automatic cleanup
|
||||||
|
private val pendingConnections = ConcurrentHashMap<String, ConnectionAttempt>()
|
||||||
|
|
||||||
|
// State management
|
||||||
|
private var isActive = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consolidated device connection information
|
||||||
|
*/
|
||||||
|
data class DeviceConnection(
|
||||||
|
val device: BluetoothDevice,
|
||||||
|
val gatt: BluetoothGatt? = null,
|
||||||
|
val characteristic: BluetoothGattCharacteristic? = null,
|
||||||
|
val rssi: Int = Int.MIN_VALUE,
|
||||||
|
val isClient: Boolean = false,
|
||||||
|
val connectedAt: Long = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connection attempt tracking with automatic expiry
|
||||||
|
*/
|
||||||
|
data class ConnectionAttempt(
|
||||||
|
val attempts: Int,
|
||||||
|
val lastAttempt: Long = System.currentTimeMillis()
|
||||||
|
) {
|
||||||
|
fun isExpired(): Boolean =
|
||||||
|
System.currentTimeMillis() - lastAttempt > CONNECTION_RETRY_DELAY * 2
|
||||||
|
|
||||||
|
fun shouldRetry(): Boolean =
|
||||||
|
attempts < MAX_CONNECTION_ATTEMPTS &&
|
||||||
|
System.currentTimeMillis() - lastAttempt > CONNECTION_RETRY_DELAY
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the connection tracker
|
||||||
|
*/
|
||||||
|
fun start() {
|
||||||
|
isActive = true
|
||||||
|
startPeriodicCleanup()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the connection tracker
|
||||||
|
*/
|
||||||
|
fun stop() {
|
||||||
|
isActive = false
|
||||||
|
cleanupAllConnections()
|
||||||
|
clearAllConnections()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a device connection
|
||||||
|
*/
|
||||||
|
fun addDeviceConnection(deviceAddress: String, deviceConn: DeviceConnection) {
|
||||||
|
connectedDevices[deviceAddress] = deviceConn
|
||||||
|
pendingConnections.remove(deviceAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a device connection
|
||||||
|
*/
|
||||||
|
fun updateDeviceConnection(deviceAddress: String, deviceConn: DeviceConnection) {
|
||||||
|
connectedDevices[deviceAddress] = deviceConn
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a device connection
|
||||||
|
*/
|
||||||
|
fun getDeviceConnection(deviceAddress: String): DeviceConnection? {
|
||||||
|
return connectedDevices[deviceAddress]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all connected devices
|
||||||
|
*/
|
||||||
|
fun getConnectedDevices(): Map<String, DeviceConnection> {
|
||||||
|
return connectedDevices.toMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get subscribed devices (for server connections)
|
||||||
|
*/
|
||||||
|
fun getSubscribedDevices(): List<BluetoothDevice> {
|
||||||
|
return subscribedDevices.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a subscribed device
|
||||||
|
*/
|
||||||
|
fun addSubscribedDevice(device: BluetoothDevice) {
|
||||||
|
subscribedDevices.add(device)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a subscribed device
|
||||||
|
*/
|
||||||
|
fun removeSubscribedDevice(device: BluetoothDevice) {
|
||||||
|
subscribedDevices.remove(device)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if device is already connected
|
||||||
|
*/
|
||||||
|
fun isDeviceConnected(deviceAddress: String): Boolean {
|
||||||
|
return connectedDevices.containsKey(deviceAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if connection attempt is allowed
|
||||||
|
*/
|
||||||
|
fun isConnectionAttemptAllowed(deviceAddress: String): Boolean {
|
||||||
|
val existingAttempt = pendingConnections[deviceAddress]
|
||||||
|
return existingAttempt?.let {
|
||||||
|
it.isExpired() || it.shouldRetry()
|
||||||
|
} ?: true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a pending connection attempt
|
||||||
|
*/
|
||||||
|
fun addPendingConnection(deviceAddress: String): Boolean {
|
||||||
|
synchronized(pendingConnections) {
|
||||||
|
// Double-check inside synchronized block
|
||||||
|
val currentAttempt = pendingConnections[deviceAddress]
|
||||||
|
if (currentAttempt != null && !currentAttempt.isExpired() && !currentAttempt.shouldRetry()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update connection attempt atomically
|
||||||
|
val attempts = (currentAttempt?.attempts ?: 0) + 1
|
||||||
|
pendingConnections[deviceAddress] = ConnectionAttempt(attempts)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a pending connection
|
||||||
|
*/
|
||||||
|
fun removePendingConnection(deviceAddress: String) {
|
||||||
|
pendingConnections.remove(deviceAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get connected device count
|
||||||
|
*/
|
||||||
|
fun getConnectedDeviceCount(): Int = connectedDevices.size
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if connection limit is reached
|
||||||
|
*/
|
||||||
|
fun isConnectionLimitReached(): Boolean {
|
||||||
|
return connectedDevices.size >= powerManager.getMaxConnections()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enforce connection limits by disconnecting oldest connections
|
||||||
|
*/
|
||||||
|
fun enforceConnectionLimits() {
|
||||||
|
val maxConnections = powerManager.getMaxConnections()
|
||||||
|
if (connectedDevices.size > maxConnections) {
|
||||||
|
Log.i(TAG, "Enforcing connection limit: ${connectedDevices.size} > $maxConnections")
|
||||||
|
|
||||||
|
// Disconnect oldest client connections first
|
||||||
|
val sortedConnections = connectedDevices.values
|
||||||
|
.filter { it.isClient }
|
||||||
|
.sortedBy { it.connectedAt }
|
||||||
|
|
||||||
|
val toDisconnect = sortedConnections.take(connectedDevices.size - maxConnections)
|
||||||
|
toDisconnect.forEach { deviceConn ->
|
||||||
|
Log.d(TAG, "Disconnecting ${deviceConn.device.address} due to connection limit")
|
||||||
|
deviceConn.gatt?.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up a specific device connection
|
||||||
|
*/
|
||||||
|
fun cleanupDeviceConnection(deviceAddress: String) {
|
||||||
|
connectedDevices.remove(deviceAddress)?.let { deviceConn ->
|
||||||
|
subscribedDevices.removeAll { it.address == deviceAddress }
|
||||||
|
addressPeerMap.remove(deviceAddress)
|
||||||
|
}
|
||||||
|
// CRITICAL FIX: Always remove from pending connections when cleaning up
|
||||||
|
// This prevents failed connections from blocking future attempts
|
||||||
|
pendingConnections.remove(deviceAddress)
|
||||||
|
Log.d(TAG, "Cleaned up device connection for $deviceAddress")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up all connections
|
||||||
|
*/
|
||||||
|
private fun cleanupAllConnections() {
|
||||||
|
connectedDevices.values.forEach { deviceConn ->
|
||||||
|
deviceConn.gatt?.disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
connectionScope.launch {
|
||||||
|
delay(CLEANUP_DELAY)
|
||||||
|
|
||||||
|
connectedDevices.values.forEach { deviceConn ->
|
||||||
|
try {
|
||||||
|
deviceConn.gatt?.close()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Error closing GATT during cleanup: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all connection tracking
|
||||||
|
*/
|
||||||
|
private fun clearAllConnections() {
|
||||||
|
connectedDevices.clear()
|
||||||
|
subscribedDevices.clear()
|
||||||
|
addressPeerMap.clear()
|
||||||
|
pendingConnections.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start periodic cleanup of expired connections
|
||||||
|
*/
|
||||||
|
private fun startPeriodicCleanup() {
|
||||||
|
connectionScope.launch {
|
||||||
|
while (isActive) {
|
||||||
|
delay(CLEANUP_INTERVAL)
|
||||||
|
|
||||||
|
if (!isActive) break
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Clean up expired pending connections
|
||||||
|
val expiredConnections = pendingConnections.filter { it.value.isExpired() }
|
||||||
|
expiredConnections.keys.forEach { pendingConnections.remove(it) }
|
||||||
|
|
||||||
|
// Log cleanup if any
|
||||||
|
if (expiredConnections.isNotEmpty()) {
|
||||||
|
Log.d(TAG, "Cleaned up ${expiredConnections.size} expired connection attempts")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log current state
|
||||||
|
Log.d(TAG, "Periodic cleanup: ${connectedDevices.size} connections, ${pendingConnections.size} pending")
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Error in periodic cleanup: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get debug information
|
||||||
|
*/
|
||||||
|
fun getDebugInfo(): String {
|
||||||
|
return buildString {
|
||||||
|
appendLine("Connected Devices: ${connectedDevices.size} / ${powerManager.getMaxConnections()}")
|
||||||
|
connectedDevices.forEach { (address, deviceConn) ->
|
||||||
|
val age = (System.currentTimeMillis() - deviceConn.connectedAt) / 1000
|
||||||
|
appendLine(" - $address (${if (deviceConn.isClient) "client" else "server"}, ${age}s, RSSI: ${deviceConn.rssi})")
|
||||||
|
}
|
||||||
|
appendLine()
|
||||||
|
appendLine("Subscribed Devices (server mode): ${subscribedDevices.size}")
|
||||||
|
appendLine()
|
||||||
|
appendLine("Pending Connections: ${pendingConnections.size}")
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
pendingConnections.forEach { (address, attempt) ->
|
||||||
|
val elapsed = (now - attempt.lastAttempt) / 1000
|
||||||
|
appendLine(" - $address: ${attempt.attempts} attempts, last ${elapsed}s ago")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,420 @@
|
|||||||
|
package com.bitchat.android.mesh
|
||||||
|
|
||||||
|
import android.bluetooth.*
|
||||||
|
import android.bluetooth.le.BluetoothLeScanner
|
||||||
|
import android.bluetooth.le.ScanCallback
|
||||||
|
import android.bluetooth.le.ScanFilter
|
||||||
|
import android.bluetooth.le.ScanResult
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.ParcelUuid
|
||||||
|
import android.util.Log
|
||||||
|
import com.bitchat.android.protocol.BitchatPacket
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages GATT client operations, scanning, and client-side connections
|
||||||
|
*/
|
||||||
|
class BluetoothGattClientManager(
|
||||||
|
private val context: Context,
|
||||||
|
private val connectionScope: CoroutineScope,
|
||||||
|
private val connectionTracker: BluetoothConnectionTracker,
|
||||||
|
private val permissionManager: BluetoothPermissionManager,
|
||||||
|
private val powerManager: PowerManager,
|
||||||
|
private val delegate: BluetoothConnectionManagerDelegate?
|
||||||
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "BluetoothGattClientManager"
|
||||||
|
// Use exact same UUIDs as iOS version
|
||||||
|
private val SERVICE_UUID = UUID.fromString("F47B5E2D-4A9E-4C5A-9B3F-8E1D2C3A4B5C")
|
||||||
|
private val CHARACTERISTIC_UUID = UUID.fromString("A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5D")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Core Bluetooth components
|
||||||
|
private val bluetoothManager: BluetoothManager =
|
||||||
|
context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
||||||
|
private val bluetoothAdapter: BluetoothAdapter? = bluetoothManager.adapter
|
||||||
|
private val bleScanner: BluetoothLeScanner? = bluetoothAdapter?.bluetoothLeScanner
|
||||||
|
|
||||||
|
// Scan management
|
||||||
|
private var scanCallback: ScanCallback? = null
|
||||||
|
|
||||||
|
// CRITICAL FIX: Scan rate limiting to prevent "scanning too frequently" errors
|
||||||
|
private var lastScanStartTime = 0L
|
||||||
|
private var lastScanStopTime = 0L
|
||||||
|
private var isCurrentlyScanning = false
|
||||||
|
private val scanRateLimit = 5000L // Minimum 5 seconds between scan start attempts
|
||||||
|
|
||||||
|
// State management
|
||||||
|
private var isActive = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start client manager
|
||||||
|
*/
|
||||||
|
fun start(): Boolean {
|
||||||
|
if (!permissionManager.hasBluetoothPermissions()) {
|
||||||
|
Log.e(TAG, "Missing Bluetooth permissions")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bluetoothAdapter?.isEnabled != true) {
|
||||||
|
Log.e(TAG, "Bluetooth is not enabled")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bleScanner == null) {
|
||||||
|
Log.e(TAG, "BLE scanner not available")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
isActive = true
|
||||||
|
|
||||||
|
connectionScope.launch {
|
||||||
|
if (powerManager.shouldUseDutyCycle()) {
|
||||||
|
Log.i(TAG, "Using power-aware duty cycling")
|
||||||
|
} else {
|
||||||
|
startScanning()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop client manager
|
||||||
|
*/
|
||||||
|
fun stop() {
|
||||||
|
isActive = false
|
||||||
|
|
||||||
|
connectionScope.launch {
|
||||||
|
stopScanning()
|
||||||
|
Log.i(TAG, "GATT client manager stopped")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle scan state changes from power manager
|
||||||
|
*/
|
||||||
|
fun onScanStateChanged(shouldScan: Boolean) {
|
||||||
|
if (shouldScan) {
|
||||||
|
startScanning()
|
||||||
|
} else {
|
||||||
|
stopScanning()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start scanning with rate limiting
|
||||||
|
*/
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
private fun startScanning() {
|
||||||
|
if (!permissionManager.hasBluetoothPermissions() || bleScanner == null || !isActive) return
|
||||||
|
|
||||||
|
// CRITICAL FIX: Rate limit scan starts to prevent "scanning too frequently" errors
|
||||||
|
val currentTime = System.currentTimeMillis()
|
||||||
|
if (isCurrentlyScanning) {
|
||||||
|
Log.d(TAG, "Scan already in progress, skipping start request")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val timeSinceLastStart = currentTime - lastScanStartTime
|
||||||
|
if (timeSinceLastStart < scanRateLimit) {
|
||||||
|
val remainingWait = scanRateLimit - timeSinceLastStart
|
||||||
|
Log.w(TAG, "Scan rate limited: need to wait ${remainingWait}ms before starting scan")
|
||||||
|
|
||||||
|
// Schedule delayed scan start
|
||||||
|
connectionScope.launch {
|
||||||
|
delay(remainingWait)
|
||||||
|
if (isActive && !isCurrentlyScanning) {
|
||||||
|
startScanning()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val scanFilter = ScanFilter.Builder()
|
||||||
|
.setServiceUuid(ParcelUuid(SERVICE_UUID))
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val scanFilters = listOf(scanFilter)
|
||||||
|
|
||||||
|
Log.d(TAG, "Starting BLE scan with target service UUID: $SERVICE_UUID")
|
||||||
|
|
||||||
|
scanCallback = object : ScanCallback() {
|
||||||
|
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
||||||
|
handleScanResult(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBatchScanResults(results: MutableList<ScanResult>) {
|
||||||
|
Log.d(TAG, "Batch scan results received: ${results.size} devices")
|
||||||
|
results.forEach { result ->
|
||||||
|
handleScanResult(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onScanFailed(errorCode: Int) {
|
||||||
|
Log.e(TAG, "Scan failed: $errorCode")
|
||||||
|
isCurrentlyScanning = false
|
||||||
|
lastScanStopTime = System.currentTimeMillis()
|
||||||
|
|
||||||
|
when (errorCode) {
|
||||||
|
1 -> Log.e(TAG, "SCAN_FAILED_ALREADY_STARTED")
|
||||||
|
2 -> Log.e(TAG, "SCAN_FAILED_APPLICATION_REGISTRATION_FAILED")
|
||||||
|
3 -> Log.e(TAG, "SCAN_FAILED_INTERNAL_ERROR")
|
||||||
|
4 -> Log.e(TAG, "SCAN_FAILED_FEATURE_UNSUPPORTED")
|
||||||
|
5 -> Log.e(TAG, "SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES")
|
||||||
|
6 -> {
|
||||||
|
Log.e(TAG, "SCAN_FAILED_SCANNING_TOO_FREQUENTLY")
|
||||||
|
Log.w(TAG, "Scan failed due to rate limiting - will retry after delay")
|
||||||
|
connectionScope.launch {
|
||||||
|
delay(10000) // Wait 10 seconds before retrying
|
||||||
|
if (isActive) {
|
||||||
|
startScanning()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> Log.e(TAG, "Unknown scan failure code: $errorCode")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
lastScanStartTime = currentTime
|
||||||
|
isCurrentlyScanning = true
|
||||||
|
|
||||||
|
bleScanner.startScan(scanFilters, powerManager.getScanSettings(), scanCallback)
|
||||||
|
Log.d(TAG, "BLE scan started successfully")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Exception starting scan: ${e.message}")
|
||||||
|
isCurrentlyScanning = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop scanning
|
||||||
|
*/
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
private fun stopScanning() {
|
||||||
|
if (!permissionManager.hasBluetoothPermissions() || bleScanner == null) return
|
||||||
|
|
||||||
|
if (isCurrentlyScanning) {
|
||||||
|
try {
|
||||||
|
scanCallback?.let {
|
||||||
|
bleScanner.stopScan(it)
|
||||||
|
Log.d(TAG, "BLE scan stopped successfully")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Error stopping scan: ${e.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
isCurrentlyScanning = false
|
||||||
|
lastScanStopTime = System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle scan result and initiate connection if appropriate
|
||||||
|
*/
|
||||||
|
private fun handleScanResult(result: ScanResult) {
|
||||||
|
val device = result.device
|
||||||
|
val rssi = result.rssi
|
||||||
|
val deviceAddress = device.address
|
||||||
|
val scanRecord = result.scanRecord
|
||||||
|
|
||||||
|
// CRITICAL: Only process devices that have our service UUID
|
||||||
|
val hasOurService = scanRecord?.serviceUuids?.any { it.uuid == SERVICE_UUID } == true
|
||||||
|
if (!hasOurService) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Power-aware RSSI filtering
|
||||||
|
if (rssi < powerManager.getRSSIThreshold()) {
|
||||||
|
Log.d(TAG, "Skipping device $deviceAddress due to weak signal: $rssi < ${powerManager.getRSSIThreshold()}")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if already connected OR already attempting to connect
|
||||||
|
if (connectionTracker.isDeviceConnected(deviceAddress)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if connection attempt is allowed
|
||||||
|
if (!connectionTracker.isConnectionAttemptAllowed(deviceAddress)) {
|
||||||
|
Log.d(TAG, "Connection to $deviceAddress not allowed due to recent attempts")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connectionTracker.isConnectionLimitReached()) {
|
||||||
|
Log.d(TAG, "Connection limit reached (${powerManager.getMaxConnections()})")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add pending connection and start connection
|
||||||
|
if (connectionTracker.addPendingConnection(deviceAddress)) {
|
||||||
|
connectToDevice(device, rssi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect to a device as GATT client
|
||||||
|
*/
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
private fun connectToDevice(device: BluetoothDevice, rssi: Int) {
|
||||||
|
if (!permissionManager.hasBluetoothPermissions()) return
|
||||||
|
|
||||||
|
val deviceAddress = device.address
|
||||||
|
Log.d(TAG, "Connecting to bitchat device: $deviceAddress")
|
||||||
|
|
||||||
|
val gattCallback = object : BluetoothGattCallback() {
|
||||||
|
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
|
||||||
|
Log.d(TAG, "Client: Connection state change - Device: $deviceAddress, Status: $status, NewState: $newState")
|
||||||
|
|
||||||
|
if (newState == BluetoothProfile.STATE_CONNECTED && status == BluetoothGatt.GATT_SUCCESS) {
|
||||||
|
Log.i(TAG, "Client: Successfully connected to $deviceAddress. Requesting MTU...")
|
||||||
|
// Request a larger MTU. Must be done before any data transfer.
|
||||||
|
connectionScope.launch {
|
||||||
|
delay(200) // A small delay can improve reliability of MTU request.
|
||||||
|
gatt.requestMtu(517)
|
||||||
|
}
|
||||||
|
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
|
||||||
|
if (status != BluetoothGatt.GATT_SUCCESS) {
|
||||||
|
Log.w(TAG, "Client: Disconnected from $deviceAddress with error status $status")
|
||||||
|
if (status == 147) {
|
||||||
|
Log.e(TAG, "Client: Connection establishment failed (status 147) for $deviceAddress")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "Client: Cleanly disconnected from $deviceAddress")
|
||||||
|
}
|
||||||
|
|
||||||
|
connectionTracker.cleanupDeviceConnection(deviceAddress)
|
||||||
|
|
||||||
|
connectionScope.launch {
|
||||||
|
delay(500) // CLEANUP_DELAY
|
||||||
|
try {
|
||||||
|
gatt.close()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Error closing GATT: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
|
||||||
|
val deviceAddress = gatt.device.address
|
||||||
|
Log.i(TAG, "Client: MTU changed for $deviceAddress to $mtu with status $status")
|
||||||
|
|
||||||
|
if (status == BluetoothGatt.GATT_SUCCESS) {
|
||||||
|
Log.i(TAG, "MTU successfully negotiated for $deviceAddress. Discovering services.")
|
||||||
|
|
||||||
|
// Now that MTU is set, connection is fully ready.
|
||||||
|
val deviceConn = BluetoothConnectionTracker.DeviceConnection(
|
||||||
|
device = gatt.device,
|
||||||
|
gatt = gatt,
|
||||||
|
rssi = rssi,
|
||||||
|
isClient = true
|
||||||
|
)
|
||||||
|
connectionTracker.addDeviceConnection(deviceAddress, deviceConn)
|
||||||
|
|
||||||
|
// Start service discovery only AFTER MTU is set.
|
||||||
|
gatt.discoverServices()
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "MTU negotiation failed for $deviceAddress with status: $status. Disconnecting.")
|
||||||
|
connectionTracker.removePendingConnection(deviceAddress)
|
||||||
|
gatt.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
|
||||||
|
if (status == BluetoothGatt.GATT_SUCCESS) {
|
||||||
|
val service = gatt.getService(SERVICE_UUID)
|
||||||
|
if (service != null) {
|
||||||
|
val characteristic = service.getCharacteristic(CHARACTERISTIC_UUID)
|
||||||
|
if (characteristic != null) {
|
||||||
|
connectionTracker.getDeviceConnection(deviceAddress)?.let { deviceConn ->
|
||||||
|
val updatedConn = deviceConn.copy(characteristic = characteristic)
|
||||||
|
connectionTracker.updateDeviceConnection(deviceAddress, updatedConn)
|
||||||
|
Log.d(TAG, "Client: Updated device connection with characteristic for $deviceAddress")
|
||||||
|
}
|
||||||
|
|
||||||
|
gatt.setCharacteristicNotification(characteristic, true)
|
||||||
|
val descriptor = characteristic.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"))
|
||||||
|
if (descriptor != null) {
|
||||||
|
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
|
||||||
|
gatt.writeDescriptor(descriptor)
|
||||||
|
|
||||||
|
connectionScope.launch {
|
||||||
|
delay(200)
|
||||||
|
Log.i(TAG, "Client: Connection setup complete for $deviceAddress")
|
||||||
|
delegate?.onDeviceConnected(device)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Client: CCCD descriptor not found for $deviceAddress")
|
||||||
|
gatt.disconnect()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Client: Required characteristic not found for $deviceAddress")
|
||||||
|
gatt.disconnect()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Client: Required service not found for $deviceAddress")
|
||||||
|
gatt.disconnect()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Client: Service discovery failed with status $status for $deviceAddress")
|
||||||
|
gatt.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
|
||||||
|
val value = characteristic.value
|
||||||
|
Log.d(TAG, "Client: Received packet from ${gatt.device.address}, size: ${value.size} bytes")
|
||||||
|
val packet = BitchatPacket.fromBinaryData(value)
|
||||||
|
if (packet != null) {
|
||||||
|
val peerID = String(packet.senderID).replace("\u0000", "")
|
||||||
|
Log.d(TAG, "Client: Parsed packet type ${packet.type} from $peerID")
|
||||||
|
delegate?.onPacketReceived(packet, peerID, gatt.device)
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Client: Failed to parse packet from ${gatt.device.address}, size: ${value.size} bytes")
|
||||||
|
Log.w(TAG, "Client: Packet data: ${value.joinToString(" ") { "%02x".format(it) }}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Log.d(TAG, "Client: Attempting GATT connection to $deviceAddress with autoConnect=false")
|
||||||
|
val gatt = device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE)
|
||||||
|
if (gatt == null) {
|
||||||
|
Log.e(TAG, "connectGatt returned null for $deviceAddress")
|
||||||
|
connectionTracker.removePendingConnection(deviceAddress)
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "Client: GATT connection initiated successfully for $deviceAddress")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Client: Exception connecting to $deviceAddress: ${e.message}")
|
||||||
|
connectionTracker.removePendingConnection(deviceAddress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restart scanning for power mode changes
|
||||||
|
*/
|
||||||
|
fun restartScanning() {
|
||||||
|
if (!isActive) return
|
||||||
|
|
||||||
|
connectionScope.launch {
|
||||||
|
stopScanning()
|
||||||
|
delay(1000) // Extra delay to avoid rate limiting
|
||||||
|
|
||||||
|
if (powerManager.shouldUseDutyCycle()) {
|
||||||
|
Log.i(TAG, "Switching to duty cycle scanning mode")
|
||||||
|
// Duty cycle will handle scanning
|
||||||
|
} else {
|
||||||
|
Log.i(TAG, "Switching to continuous scanning mode")
|
||||||
|
startScanning()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,321 @@
|
|||||||
|
package com.bitchat.android.mesh
|
||||||
|
|
||||||
|
import android.bluetooth.*
|
||||||
|
import android.bluetooth.le.AdvertiseCallback
|
||||||
|
import android.bluetooth.le.AdvertiseData
|
||||||
|
import android.bluetooth.le.AdvertiseSettings
|
||||||
|
import android.bluetooth.le.BluetoothLeAdvertiser
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.ParcelUuid
|
||||||
|
import android.util.Log
|
||||||
|
import com.bitchat.android.protocol.BitchatPacket
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages GATT server operations, advertising, and server-side connections
|
||||||
|
*/
|
||||||
|
class BluetoothGattServerManager(
|
||||||
|
private val context: Context,
|
||||||
|
private val connectionScope: CoroutineScope,
|
||||||
|
private val connectionTracker: BluetoothConnectionTracker,
|
||||||
|
private val permissionManager: BluetoothPermissionManager,
|
||||||
|
private val powerManager: PowerManager,
|
||||||
|
private val delegate: BluetoothConnectionManagerDelegate?
|
||||||
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "BluetoothGattServerManager"
|
||||||
|
// Use exact same UUIDs as iOS version
|
||||||
|
private val SERVICE_UUID = UUID.fromString("F47B5E2D-4A9E-4C5A-9B3F-8E1D2C3A4B5C")
|
||||||
|
private val CHARACTERISTIC_UUID = UUID.fromString("A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5D")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Core Bluetooth components
|
||||||
|
private val bluetoothManager: BluetoothManager =
|
||||||
|
context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
||||||
|
private val bluetoothAdapter: BluetoothAdapter? = bluetoothManager.adapter
|
||||||
|
private val bleAdvertiser: BluetoothLeAdvertiser? = bluetoothAdapter?.bluetoothLeAdvertiser
|
||||||
|
|
||||||
|
// GATT server for peripheral mode
|
||||||
|
private var gattServer: BluetoothGattServer? = null
|
||||||
|
private var characteristic: BluetoothGattCharacteristic? = null
|
||||||
|
private var advertiseCallback: AdvertiseCallback? = null
|
||||||
|
|
||||||
|
// State management
|
||||||
|
private var isActive = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start GATT server
|
||||||
|
*/
|
||||||
|
fun start(): Boolean {
|
||||||
|
if (!permissionManager.hasBluetoothPermissions()) {
|
||||||
|
Log.e(TAG, "Missing Bluetooth permissions")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bluetoothAdapter?.isEnabled != true) {
|
||||||
|
Log.e(TAG, "Bluetooth is not enabled")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bleAdvertiser == null) {
|
||||||
|
Log.e(TAG, "BLE advertiser not available")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
isActive = true
|
||||||
|
|
||||||
|
connectionScope.launch {
|
||||||
|
setupGattServer()
|
||||||
|
delay(300) // Brief delay to ensure GATT server is ready
|
||||||
|
startAdvertising()
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop GATT server
|
||||||
|
*/
|
||||||
|
fun stop() {
|
||||||
|
isActive = false
|
||||||
|
|
||||||
|
connectionScope.launch {
|
||||||
|
stopAdvertising()
|
||||||
|
|
||||||
|
// Close GATT server
|
||||||
|
gattServer?.close()
|
||||||
|
gattServer = null
|
||||||
|
|
||||||
|
Log.i(TAG, "GATT server stopped")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get GATT server instance
|
||||||
|
*/
|
||||||
|
fun getGattServer(): BluetoothGattServer? = gattServer
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get characteristic instance
|
||||||
|
*/
|
||||||
|
fun getCharacteristic(): BluetoothGattCharacteristic? = characteristic
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup GATT server with proper sequencing
|
||||||
|
*/
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
private fun setupGattServer() {
|
||||||
|
if (!permissionManager.hasBluetoothPermissions()) return
|
||||||
|
|
||||||
|
val serverCallback = object : BluetoothGattServerCallback() {
|
||||||
|
override fun onConnectionStateChange(device: BluetoothDevice, status: Int, newState: Int) {
|
||||||
|
// Guard against callbacks after service shutdown
|
||||||
|
if (!isActive) {
|
||||||
|
Log.d(TAG, "Server: Ignoring connection state change after shutdown")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
when (newState) {
|
||||||
|
BluetoothProfile.STATE_CONNECTED -> {
|
||||||
|
Log.d(TAG, "Server: Device connected ${device.address}")
|
||||||
|
val deviceConn = BluetoothConnectionTracker.DeviceConnection(
|
||||||
|
device = device,
|
||||||
|
isClient = false
|
||||||
|
)
|
||||||
|
connectionTracker.addDeviceConnection(device.address, deviceConn)
|
||||||
|
}
|
||||||
|
BluetoothProfile.STATE_DISCONNECTED -> {
|
||||||
|
Log.d(TAG, "Server: Device disconnected ${device.address}")
|
||||||
|
connectionTracker.cleanupDeviceConnection(device.address)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceAdded(status: Int, service: BluetoothGattService) {
|
||||||
|
// Guard against callbacks after service shutdown
|
||||||
|
if (!isActive) {
|
||||||
|
Log.d(TAG, "Server: Ignoring service added callback after shutdown")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status == BluetoothGatt.GATT_SUCCESS) {
|
||||||
|
Log.d(TAG, "Server: Service added successfully: ${service.uuid}")
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Server: Failed to add service: ${service.uuid}, status: $status")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCharacteristicWriteRequest(
|
||||||
|
device: BluetoothDevice,
|
||||||
|
requestId: Int,
|
||||||
|
characteristic: BluetoothGattCharacteristic,
|
||||||
|
preparedWrite: Boolean,
|
||||||
|
responseNeeded: Boolean,
|
||||||
|
offset: Int,
|
||||||
|
value: ByteArray
|
||||||
|
) {
|
||||||
|
// Guard against callbacks after service shutdown
|
||||||
|
if (!isActive) {
|
||||||
|
Log.d(TAG, "Server: Ignoring characteristic write after shutdown")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (characteristic.uuid == CHARACTERISTIC_UUID) {
|
||||||
|
Log.d(TAG, "Server: Received packet from ${device.address}, size: ${value.size} bytes")
|
||||||
|
val packet = BitchatPacket.fromBinaryData(value)
|
||||||
|
if (packet != null) {
|
||||||
|
val peerID = String(packet.senderID).replace("\u0000", "")
|
||||||
|
Log.d(TAG, "Server: Parsed packet type ${packet.type} from $peerID")
|
||||||
|
delegate?.onPacketReceived(packet, peerID, device)
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Server: Failed to parse packet from ${device.address}, size: ${value.size} bytes")
|
||||||
|
Log.w(TAG, "Server: Packet data: ${value.joinToString(" ") { "%02x".format(it) }}")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responseNeeded) {
|
||||||
|
gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDescriptorWriteRequest(
|
||||||
|
device: BluetoothDevice,
|
||||||
|
requestId: Int,
|
||||||
|
descriptor: BluetoothGattDescriptor,
|
||||||
|
preparedWrite: Boolean,
|
||||||
|
responseNeeded: Boolean,
|
||||||
|
offset: Int,
|
||||||
|
value: ByteArray
|
||||||
|
) {
|
||||||
|
// Guard against callbacks after service shutdown
|
||||||
|
if (!isActive) {
|
||||||
|
Log.d(TAG, "Server: Ignoring descriptor write after shutdown")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE.contentEquals(value)) {
|
||||||
|
Log.d(TAG, "Device ${device.address} subscribed to notifications")
|
||||||
|
connectionTracker.addSubscribedDevice(device)
|
||||||
|
|
||||||
|
connectionScope.launch {
|
||||||
|
delay(100)
|
||||||
|
if (isActive) { // Check if still active
|
||||||
|
delegate?.onDeviceConnected(device)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responseNeeded) {
|
||||||
|
gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proper cleanup sequencing to prevent race conditions
|
||||||
|
gattServer?.let { server ->
|
||||||
|
Log.d(TAG, "Cleaning up existing GATT server")
|
||||||
|
try {
|
||||||
|
server.close()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Error closing existing GATT server: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Small delay to ensure cleanup is complete
|
||||||
|
Thread.sleep(100)
|
||||||
|
|
||||||
|
if (!isActive) {
|
||||||
|
Log.d(TAG, "Service inactive, skipping GATT server creation")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new server
|
||||||
|
gattServer = bluetoothManager.openGattServer(context, serverCallback)
|
||||||
|
|
||||||
|
// Create characteristic with notification support
|
||||||
|
characteristic = BluetoothGattCharacteristic(
|
||||||
|
CHARACTERISTIC_UUID,
|
||||||
|
BluetoothGattCharacteristic.PROPERTY_READ or
|
||||||
|
BluetoothGattCharacteristic.PROPERTY_WRITE or
|
||||||
|
BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE or
|
||||||
|
BluetoothGattCharacteristic.PROPERTY_NOTIFY,
|
||||||
|
BluetoothGattCharacteristic.PERMISSION_READ or
|
||||||
|
BluetoothGattCharacteristic.PERMISSION_WRITE
|
||||||
|
)
|
||||||
|
|
||||||
|
val descriptor = BluetoothGattDescriptor(
|
||||||
|
UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"),
|
||||||
|
BluetoothGattDescriptor.PERMISSION_READ or BluetoothGattDescriptor.PERMISSION_WRITE
|
||||||
|
)
|
||||||
|
characteristic?.addDescriptor(descriptor)
|
||||||
|
|
||||||
|
val service = BluetoothGattService(SERVICE_UUID, BluetoothGattService.SERVICE_TYPE_PRIMARY)
|
||||||
|
service.addCharacteristic(characteristic)
|
||||||
|
|
||||||
|
gattServer?.addService(service)
|
||||||
|
|
||||||
|
Log.i(TAG, "GATT server setup complete")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start advertising
|
||||||
|
*/
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
private fun startAdvertising() {
|
||||||
|
if (!permissionManager.hasBluetoothPermissions() || bleAdvertiser == null || !isActive) return
|
||||||
|
|
||||||
|
val settings = powerManager.getAdvertiseSettings()
|
||||||
|
|
||||||
|
val data = AdvertiseData.Builder()
|
||||||
|
.addServiceUuid(ParcelUuid(SERVICE_UUID))
|
||||||
|
.setIncludeTxPowerLevel(false)
|
||||||
|
.setIncludeDeviceName(false)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
advertiseCallback = object : AdvertiseCallback() {
|
||||||
|
override fun onStartSuccess(settingsInEffect: AdvertiseSettings) {
|
||||||
|
Log.i(TAG, "Advertising started (power mode: ${powerManager.getPowerInfo().split("Current Mode: ")[1].split("\n")[0]})")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartFailure(errorCode: Int) {
|
||||||
|
Log.e(TAG, "Advertising failed: $errorCode")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
bleAdvertiser.startAdvertising(settings, data, advertiseCallback)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Exception starting advertising: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop advertising
|
||||||
|
*/
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
private fun stopAdvertising() {
|
||||||
|
if (!permissionManager.hasBluetoothPermissions() || bleAdvertiser == null) return
|
||||||
|
try {
|
||||||
|
advertiseCallback?.let { bleAdvertiser.stopAdvertising(it) }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Error stopping advertising: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restart advertising (for power mode changes)
|
||||||
|
*/
|
||||||
|
fun restartAdvertising() {
|
||||||
|
if (!isActive) return
|
||||||
|
|
||||||
|
connectionScope.launch {
|
||||||
|
stopAdvertising()
|
||||||
|
delay(100)
|
||||||
|
startAdvertising()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
package com.bitchat.android.mesh
|
||||||
|
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import android.bluetooth.BluetoothGatt
|
||||||
|
import android.bluetooth.BluetoothGattCharacteristic
|
||||||
|
import android.bluetooth.BluetoothGattServer
|
||||||
|
import android.util.Log
|
||||||
|
import com.bitchat.android.protocol.SpecialRecipients
|
||||||
|
import com.bitchat.android.model.RoutedPacket
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles packet broadcasting to connected devices
|
||||||
|
*/
|
||||||
|
class BluetoothPacketBroadcaster(
|
||||||
|
private val connectionScope: CoroutineScope,
|
||||||
|
private val connectionTracker: BluetoothConnectionTracker
|
||||||
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "BluetoothPacketBroadcaster"
|
||||||
|
private const val CLEANUP_DELAY = 500L
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcast packet to connected devices with connection limit enforcement
|
||||||
|
*/
|
||||||
|
fun broadcastPacket(
|
||||||
|
routed: RoutedPacket,
|
||||||
|
gattServer: BluetoothGattServer?,
|
||||||
|
characteristic: BluetoothGattCharacteristic?
|
||||||
|
) {
|
||||||
|
val packet = routed.packet
|
||||||
|
val data = packet.toBinaryData() ?: return
|
||||||
|
|
||||||
|
if (packet.recipientID != SpecialRecipients.BROADCAST) {
|
||||||
|
val recipientID = packet.recipientID?.let {
|
||||||
|
String(it).replace("\u0000", "").trim()
|
||||||
|
} ?: ""
|
||||||
|
|
||||||
|
// Try to find the recipient in server connections (subscribedDevices)
|
||||||
|
val targetDevice = connectionTracker.getSubscribedDevices()
|
||||||
|
.firstOrNull { connectionTracker.addressPeerMap[it.address] == recipientID }
|
||||||
|
|
||||||
|
// If found, send directly
|
||||||
|
if (targetDevice != null) {
|
||||||
|
Log.d(TAG, "Send packet type ${packet.type} directly to target device for recipient $recipientID: ${targetDevice.address}")
|
||||||
|
if (notifyDevice(targetDevice, data, gattServer, characteristic))
|
||||||
|
return // Sent, no need to continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to find the recipient in client connections (connectedDevices)
|
||||||
|
val targetDeviceConn = connectionTracker.getConnectedDevices().values
|
||||||
|
.firstOrNull { connectionTracker.addressPeerMap[it.device.address] == recipientID }
|
||||||
|
|
||||||
|
// If found, send directly
|
||||||
|
if (targetDeviceConn != null) {
|
||||||
|
Log.d(TAG, "Send packet type ${packet.type} directly to target client connection for recipient $recipientID: ${targetDeviceConn.device.address}")
|
||||||
|
if (writeToDeviceConn(targetDeviceConn, data))
|
||||||
|
return // Sent, no need to continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Else, continue with broadcasting to all devices
|
||||||
|
val subscribedDevices = connectionTracker.getSubscribedDevices()
|
||||||
|
val connectedDevices = connectionTracker.getConnectedDevices()
|
||||||
|
|
||||||
|
Log.d(TAG, "Broadcasting packet type ${packet.type} to ${subscribedDevices.size} server + ${connectedDevices.size} client connections")
|
||||||
|
|
||||||
|
val senderID = String(packet.senderID).replace("\u0000", "")
|
||||||
|
|
||||||
|
// Send to server connections (devices connected to our GATT server)
|
||||||
|
subscribedDevices.forEach { device ->
|
||||||
|
if (device.address == routed.relayAddress) {
|
||||||
|
Log.d(TAG, "Skipping broadcast back to relayer: ${device.address}")
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
if (connectionTracker.addressPeerMap[device.address] == senderID) {
|
||||||
|
Log.d(TAG, "Skipping broadcast back to sender: ${device.address}")
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
notifyDevice(device, data, gattServer, characteristic)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send to client connections
|
||||||
|
connectedDevices.values.forEach { deviceConn ->
|
||||||
|
if (deviceConn.isClient && deviceConn.gatt != null && deviceConn.characteristic != null) {
|
||||||
|
if (deviceConn.device.address == routed.relayAddress) {
|
||||||
|
Log.d(TAG, "Skipping broadcast back to relayer: ${deviceConn.device.address}")
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
if (connectionTracker.addressPeerMap[deviceConn.device.address] == senderID) {
|
||||||
|
Log.d(TAG, "Skipping broadcast back to sender: ${deviceConn.device.address}")
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
writeToDeviceConn(deviceConn, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send data to a single device (server side)
|
||||||
|
*/
|
||||||
|
private fun notifyDevice(
|
||||||
|
device: BluetoothDevice,
|
||||||
|
data: ByteArray,
|
||||||
|
gattServer: BluetoothGattServer?,
|
||||||
|
characteristic: BluetoothGattCharacteristic?
|
||||||
|
): Boolean {
|
||||||
|
return try {
|
||||||
|
characteristic?.let { char ->
|
||||||
|
char.value = data
|
||||||
|
val result = gattServer?.notifyCharacteristicChanged(device, char, false) ?: false
|
||||||
|
result
|
||||||
|
} ?: false
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Error sending to server connection ${device.address}: ${e.message}")
|
||||||
|
connectionScope.launch {
|
||||||
|
delay(CLEANUP_DELAY)
|
||||||
|
connectionTracker.removeSubscribedDevice(device)
|
||||||
|
connectionTracker.addressPeerMap.remove(device.address)
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send data to a single device (client side)
|
||||||
|
*/
|
||||||
|
private fun writeToDeviceConn(
|
||||||
|
deviceConn: BluetoothConnectionTracker.DeviceConnection,
|
||||||
|
data: ByteArray
|
||||||
|
): Boolean {
|
||||||
|
return try {
|
||||||
|
deviceConn.characteristic?.let { char ->
|
||||||
|
char.value = data
|
||||||
|
val result = deviceConn.gatt?.writeCharacteristic(char) ?: false
|
||||||
|
result
|
||||||
|
} ?: false
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Error sending to client connection ${deviceConn.device.address}: ${e.message}")
|
||||||
|
connectionScope.launch {
|
||||||
|
delay(CLEANUP_DELAY)
|
||||||
|
connectionTracker.cleanupDeviceConnection(deviceConn.device.address)
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package com.bitchat.android.mesh
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles all Bluetooth permission checking logic
|
||||||
|
*/
|
||||||
|
class BluetoothPermissionManager(private val context: Context) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if all required Bluetooth permissions are granted
|
||||||
|
*/
|
||||||
|
fun hasBluetoothPermissions(): Boolean {
|
||||||
|
val permissions = mutableListOf<String>()
|
||||||
|
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
|
||||||
|
permissions.addAll(listOf(
|
||||||
|
Manifest.permission.BLUETOOTH_ADVERTISE,
|
||||||
|
Manifest.permission.BLUETOOTH_CONNECT,
|
||||||
|
Manifest.permission.BLUETOOTH_SCAN
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
permissions.addAll(listOf(
|
||||||
|
Manifest.permission.BLUETOOTH,
|
||||||
|
Manifest.permission.BLUETOOTH_ADMIN
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
permissions.addAll(listOf(
|
||||||
|
Manifest.permission.ACCESS_COARSE_LOCATION,
|
||||||
|
Manifest.permission.ACCESS_FINE_LOCATION
|
||||||
|
))
|
||||||
|
|
||||||
|
return permissions.all {
|
||||||
|
ActivityCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user