power saving

This commit is contained in:
callebtc
2025-07-09 15:47:54 +02:00
parent d3e258b442
commit dd8aa083b9
5 changed files with 675 additions and 351 deletions
@@ -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<String>()
@@ -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<String, BluetoothDevice>()
private val deviceCharacteristics = ConcurrentHashMap<BluetoothDevice, BluetoothGattCharacteristic>()
// Simplified connection tracking - reduced memory footprint
private val connectedDevices = ConcurrentHashMap<String, DeviceConnection>()
private val subscribedDevices = CopyOnWriteArrayList<BluetoothDevice>()
private val gattConnections = ConcurrentHashMap<BluetoothDevice, BluetoothGatt>() // Track GATT client connections
private val peripheralRSSI = ConcurrentHashMap<String, Int>() // Track RSSI by device address during discovery
// FIXED: Track connection attempts to prevent duplicates
private val pendingConnections = ConcurrentHashMap<String, Long>() // device address -> attempt timestamp
private val connectionAttempts = ConcurrentHashMap<String, Int>() // device address -> attempt count
private val maxConnectionAttempts = 3
private val connectionRetryDelay = 5000L // 5 seconds
// Connection attempt tracking with automatic cleanup
private val pendingConnections = ConcurrentHashMap<String, ConnectionAttempt>()
// 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<String>()
@@ -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()
}
}
@@ -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)
@@ -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)
}
@@ -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) {