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.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.lifecycleScope
import com.bitchat.android.ui.ChatScreen import com.bitchat.android.ui.ChatScreen
import com.bitchat.android.ui.ChatViewModel import com.bitchat.android.ui.ChatViewModel
import com.bitchat.android.ui.theme.BitchatTheme import com.bitchat.android.ui.theme.BitchatTheme
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() { 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() { private fun requestPermissions() {
val permissions = mutableListOf<String>() val permissions = mutableListOf<String>()
@@ -15,45 +15,52 @@ import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.CopyOnWriteArrayList
/** /**
* Manages Bluetooth connections, advertising, and scanning * Power-optimized Bluetooth connection manager with comprehensive memory management
* Extracted from BluetoothMeshService for better separation of concerns * 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 { companion object {
private const val TAG = "BluetoothConnectionManager" 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 SERVICE_UUID = UUID.fromString("F47B5E2D-4A9E-4C5A-9B3F-8E1D2C3A4B5C")
private val CHARACTERISTIC_UUID = UUID.fromString("A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5D") 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 // 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 bluetoothAdapter: BluetoothAdapter? = bluetoothManager.adapter
private val bleScanner: BluetoothLeScanner? = bluetoothAdapter?.bluetoothLeScanner private val bleScanner: BluetoothLeScanner? = bluetoothAdapter?.bluetoothLeScanner
private val bleAdvertiser: BluetoothLeAdvertiser? = bluetoothAdapter?.bluetoothLeAdvertiser private val bleAdvertiser: BluetoothLeAdvertiser? = bluetoothAdapter?.bluetoothLeAdvertiser
// Power management
private val powerManager = PowerManager(context)
// GATT server for peripheral mode // GATT server for peripheral mode
private var gattServer: BluetoothGattServer? = null private var gattServer: BluetoothGattServer? = null
private var characteristic: BluetoothGattCharacteristic? = null private var characteristic: BluetoothGattCharacteristic? = null
// Connection tracking - FIXED to properly track both server and client connections // Simplified connection tracking - reduced memory footprint
private val connectedDevices = ConcurrentHashMap<String, BluetoothDevice>() private val connectedDevices = ConcurrentHashMap<String, DeviceConnection>()
private val deviceCharacteristics = ConcurrentHashMap<BluetoothDevice, BluetoothGattCharacteristic>()
private val subscribedDevices = CopyOnWriteArrayList<BluetoothDevice>() 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 // Connection attempt tracking with automatic cleanup
private val pendingConnections = ConcurrentHashMap<String, Long>() // device address -> attempt timestamp private val pendingConnections = ConcurrentHashMap<String, ConnectionAttempt>()
private val connectionAttempts = ConcurrentHashMap<String, Int>() // device address -> attempt count
private val maxConnectionAttempts = 3
private val connectionRetryDelay = 5000L // 5 seconds
// Service registration tracking // Service state
private var serviceRegistered = false private var isActive = false
private var advertisingActive = false private var scanCallback: ScanCallback? = null
private var scanningActive = false private var advertiseCallback: AdvertiseCallback? = null
// Delegate for callbacks // Delegate for callbacks
var delegate: BluetoothConnectionManagerDelegate? = null var delegate: BluetoothConnectionManagerDelegate? = null
@@ -62,10 +69,41 @@ class BluetoothConnectionManager(private val context: Context, private val myPee
private val connectionScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) 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 { fun startServices(): Boolean {
Log.i(TAG, "Starting Bluetooth services...") Log.i(TAG, "Starting power-optimized Bluetooth services...")
if (!hasBluetoothPermissions()) { if (!hasBluetoothPermissions()) {
Log.e(TAG, "Missing Bluetooth permissions") Log.e(TAG, "Missing Bluetooth permissions")
@@ -83,19 +121,26 @@ class BluetoothConnectionManager(private val context: Context, private val myPee
} }
try { try {
isActive = true
setupGattServer() setupGattServer()
// Start services in sequence // Start power manager and services
connectionScope.launch { connectionScope.launch {
powerManager.start()
delay(500) // Ensure GATT server is ready delay(500) // Ensure GATT server is ready
startAdvertising() startAdvertising()
delay(200) delay(200)
startScanning() if (powerManager.shouldUseDutyCycle()) {
delay(200) 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 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() { fun stopServices() {
Log.i(TAG, "Stopping Bluetooth services") Log.i(TAG, "Stopping power-optimized Bluetooth services")
isActive = false
connectionScope.launch { connectionScope.launch {
// Cleanup all GATT client connections // Stop power manager first
gattConnections.values.forEach { gatt -> powerManager.stop()
try {
gatt.disconnect()
gatt.close()
} catch (e: Exception) {
Log.w(TAG, "Error closing GATT connection: ${e.message}")
}
}
// Stop advertising and scanning // Stop scanning and advertising
stopAdvertising()
stopScanning() stopScanning()
stopAdvertising()
// Cleanup all GATT connections with delay
cleanupAllConnections()
// Close GATT server // Close GATT server
gattServer?.close() gattServer?.close()
gattServer = null
// Clear all connection tracking // Clear tracking
connectedDevices.clear() clearAllConnections()
deviceCharacteristics.clear()
subscribedDevices.clear()
gattConnections.clear()
peripheralRSSI.clear()
connectionScope.cancel() 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) { fun broadcastPacket(packet: BitchatPacket) {
if (!isActive) return
val data = packet.toBinaryData() ?: 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 -> subscribedDevices.forEach { device ->
try { try {
characteristic?.let { char -> characteristic?.let { char ->
char.value = data char.value = data
val success = gattServer?.notifyCharacteristicChanged(device, char, false) ?: false gattServer?.notifyCharacteristicChanged(device, char, 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}")
}
} }
} catch (e: Exception) { } 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 // Send to client connections
gattConnections.forEach { (device, gatt) -> connectedDevices.values.forEach { deviceConn ->
try { if (deviceConn.isClient && deviceConn.gatt != null && deviceConn.characteristic != null) {
val characteristic = deviceCharacteristics[device] try {
if (characteristic != null) { deviceConn.characteristic.value = data
characteristic.value = data deviceConn.gatt.writeCharacteristic(deviceConn.characteristic)
val success = gatt.writeCharacteristic(characteristic) } catch (e: Exception) {
if (success) { Log.w(TAG, "Error sending to client connection ${deviceConn.device.address}: ${e.message}")
Log.d(TAG, "Sent packet to client connection: ${device.address}") // Clean up failed connection
} else { connectionScope.launch {
Log.w(TAG, "Failed to send packet to client connection: ${device.address}") 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 * Get connected device count
*/ */
fun getConnectedDeviceCount(): Int { fun getConnectedDeviceCount(): Int = connectedDevices.size
return connectedDevices.size
}
/** /**
* Get debug information * Get debug information including power management
*/ */
fun getDebugInfo(): String { fun getDebugInfo(): String {
return buildString { return buildString {
appendLine("=== Bluetooth Connection Manager Debug Info ===") appendLine("=== Power-Optimized Bluetooth Connection Manager ===")
appendLine("Active: $isActive")
appendLine("Bluetooth Enabled: ${bluetoothAdapter?.isEnabled}") appendLine("Bluetooth Enabled: ${bluetoothAdapter?.isEnabled}")
appendLine("Has Permissions: ${hasBluetoothPermissions()}") appendLine("Has Permissions: ${hasBluetoothPermissions()}")
appendLine("BLE Scanner Available: ${bleScanner != null}")
appendLine("BLE Advertiser Available: ${bleAdvertiser != null}")
appendLine("GATT Server Active: ${gattServer != null}") appendLine("GATT Server Active: ${gattServer != null}")
appendLine("Service Registered: $serviceRegistered")
appendLine("Advertising Active: $advertisingActive")
appendLine("Scanning Active: $scanningActive")
appendLine() appendLine()
appendLine("Connected Devices: ${connectedDevices.size}") appendLine(powerManager.getPowerInfo())
connectedDevices.forEach { (address, device) ->
appendLine(" - $address")
}
appendLine() appendLine()
appendLine("GATT Client Connections: ${gattConnections.size}") appendLine("Connected Devices: ${connectedDevices.size} / ${powerManager.getMaxConnections()}")
gattConnections.keys.forEach { device -> connectedDevices.forEach { (address, deviceConn) ->
appendLine(" - ${device.address}") val age = (System.currentTimeMillis() - deviceConn.connectedAt) / 1000
appendLine(" - $address (${if (deviceConn.isClient) "client" else "server"}, ${age}s, RSSI: ${deviceConn.rssi})")
} }
appendLine() appendLine()
appendLine("Subscribed Devices (server mode): ${subscribedDevices.size}") appendLine("Subscribed Devices (server mode): ${subscribedDevices.size}")
subscribedDevices.forEach { device ->
appendLine(" - ${device.address}")
}
appendLine() appendLine()
appendLine("Pending Connections: ${pendingConnections.size}") appendLine("Pending Connections: ${pendingConnections.size}")
pendingConnections.forEach { (address, timestamp) -> val now = System.currentTimeMillis()
val elapsed = (System.currentTimeMillis() - timestamp) / 1000 pendingConnections.forEach { (address, attempt) ->
appendLine(" - $address (${elapsed}s ago)") val elapsed = (now - attempt.lastAttempt) / 1000
} appendLine(" - $address: ${attempt.attempts} attempts, last ${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")
} }
} }
} }
/** // MARK: - PowerManagerDelegate Implementation
* Check if we have the required Bluetooth permissions
*/ 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 { private fun hasBluetoothPermissions(): Boolean {
val permissions = mutableListOf<String>() 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") @Suppress("DEPRECATION")
private fun setupGattServer() { private fun setupGattServer() {
if (!hasBluetoothPermissions()) return 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) { override fun onConnectionStateChange(device: BluetoothDevice, status: Int, newState: Int) {
when (newState) { when (newState) {
BluetoothProfile.STATE_CONNECTED -> { BluetoothProfile.STATE_CONNECTED -> {
Log.d(TAG, "Device connected to server: ${device.address}") Log.d(TAG, "Server: Device connected ${device.address}")
connectedDevices[device.address] = device val deviceConn = DeviceConnection(
device = device,
isClient = false
)
connectedDevices[device.address] = deviceConn
} }
BluetoothProfile.STATE_DISCONNECTED -> { BluetoothProfile.STATE_DISCONNECTED -> {
Log.d(TAG, "Device disconnected from server: ${device.address}") Log.d(TAG, "Server: Device disconnected ${device.address}")
connectedDevices.remove(device.address) cleanupDeviceConnection(device.address)
deviceCharacteristics.remove(device)
subscribedDevices.remove(device)
} }
} }
} }
@@ -304,8 +361,6 @@ class BluetoothConnectionManager(private val context: Context, private val myPee
value: ByteArray value: ByteArray
) { ) {
if (characteristic.uuid == CHARACTERISTIC_UUID) { if (characteristic.uuid == CHARACTERISTIC_UUID) {
Log.d(TAG, "Received write request from ${device.address}, ${value.size} bytes")
val packet = BitchatPacket.fromBinaryData(value) val packet = BitchatPacket.fromBinaryData(value)
if (packet != null) { if (packet != null) {
val peerID = String(packet.senderID).replace("\u0000", "") 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)) { if (BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE.contentEquals(value)) {
Log.d(TAG, "Device ${device.address} subscribed to notifications") Log.d(TAG, "Device ${device.address} subscribed to notifications")
if (!subscribedDevices.contains(device)) { subscribedDevices.add(device)
subscribedDevices.add(device)
// Notify delegate about new connection connectionScope.launch {
connectionScope.launch { delay(100)
delay(100) // Ensure connection is stable delegate?.onDeviceConnected(device)
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() gattServer?.close()
clearAllConnections()
gattServer = bluetoothManager.openGattServer(context, serverCallback) gattServer = bluetoothManager.openGattServer(context, serverCallback)
// Create characteristic // Create characteristic with notification support
characteristic = BluetoothGattCharacteristic( characteristic = BluetoothGattCharacteristic(
CHARACTERISTIC_UUID, CHARACTERISTIC_UUID,
BluetoothGattCharacteristic.PROPERTY_READ or BluetoothGattCharacteristic.PROPERTY_READ or
@@ -363,14 +414,12 @@ class BluetoothConnectionManager(private val context: Context, private val myPee
BluetoothGattCharacteristic.PERMISSION_WRITE BluetoothGattCharacteristic.PERMISSION_WRITE
) )
// Add notification descriptor
val descriptor = BluetoothGattDescriptor( val descriptor = BluetoothGattDescriptor(
UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"), UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"),
BluetoothGattDescriptor.PERMISSION_READ or BluetoothGattDescriptor.PERMISSION_WRITE BluetoothGattDescriptor.PERMISSION_READ or BluetoothGattDescriptor.PERMISSION_WRITE
) )
characteristic?.addDescriptor(descriptor) characteristic?.addDescriptor(descriptor)
// Create service
val service = BluetoothGattService(SERVICE_UUID, BluetoothGattService.SERVICE_TYPE_PRIMARY) val service = BluetoothGattService(SERVICE_UUID, BluetoothGattService.SERVICE_TYPE_PRIMARY)
service.addCharacteristic(characteristic) service.addCharacteristic(characteristic)
@@ -379,49 +428,25 @@ class BluetoothConnectionManager(private val context: Context, private val myPee
Log.i(TAG, "GATT server setup complete") Log.i(TAG, "GATT server setup complete")
} }
/**
* Start BLE advertising
*/
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
private fun startAdvertising() { private fun startAdvertising() {
if (!hasBluetoothPermissions() || bleAdvertiser == null) { if (!hasBluetoothPermissions() || bleAdvertiser == null || !isActive) return
Log.e(TAG, "Cannot start advertising: missing permissions or advertiser unavailable")
return
}
val settings = AdvertiseSettings.Builder() val settings = powerManager.getAdvertiseSettings()
.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED)
.setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM)
.setConnectable(true)
.setTimeout(0)
.build()
val data = AdvertiseData.Builder() val data = AdvertiseData.Builder()
.addServiceUuid(ParcelUuid(SERVICE_UUID)) .addServiceUuid(ParcelUuid(SERVICE_UUID))
.addServiceData(ParcelUuid(SERVICE_UUID), myPeerID.toByteArray(Charsets.UTF_8))
.setIncludeTxPowerLevel(false) .setIncludeTxPowerLevel(false)
.setIncludeDeviceName(false) .setIncludeDeviceName(false)
.build() .build()
val advertiseCallback = object : AdvertiseCallback() { advertiseCallback = object : AdvertiseCallback() {
override fun onStartSuccess(settingsInEffect: AdvertiseSettings) { 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) { override fun onStartFailure(errorCode: Int) {
val errorMessage = when (errorCode) { Log.e(TAG, "Advertising failed: $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()
}
} }
} }
@@ -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") @Suppress("DEPRECATION")
private fun stopAdvertising() { private fun stopAdvertising() {
if (!hasBluetoothPermissions() || bleAdvertiser == null) return 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") @Suppress("DEPRECATION")
private fun startScanning() { private fun startScanning() {
if (!hasBluetoothPermissions() || bleScanner == null) { if (!hasBluetoothPermissions() || bleScanner == null || !isActive) return
Log.e(TAG, "Cannot start scanning: missing permissions or scanner unavailable")
return
}
val scanFilter = ScanFilter.Builder() val scanFilter = ScanFilter.Builder()
.setServiceUuid(ParcelUuid(SERVICE_UUID)) .setServiceUuid(ParcelUuid(SERVICE_UUID))
.build() .build()
val scanSettings = ScanSettings.Builder() scanCallback = object : ScanCallback() {
.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() {
override fun onScanResult(callbackType: Int, result: ScanResult) { override fun onScanResult(callbackType: Int, result: ScanResult) {
handleScanResult(result) handleScanResult(result)
} }
@@ -510,248 +485,240 @@ class BluetoothConnectionManager(private val context: Context, private val myPee
} }
override fun onScanFailed(errorCode: Int) { override fun onScanFailed(errorCode: Int) {
val errorMessage = when (errorCode) { Log.e(TAG, "Scan failed: $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")
} }
} }
try { try {
bleScanner.startScan(listOf(scanFilter), scanSettings, scanCallback) bleScanner.startScan(listOf(scanFilter), powerManager.getScanSettings(), scanCallback)
Log.i(TAG, "Started BLE scanning")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Exception starting scan: ${e.message}") Log.e(TAG, "Exception starting scan: ${e.message}")
} }
} }
/**
* Stop BLE scanning
*/
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
private fun stopScanning() { private fun stopScanning() {
if (!hasBluetoothPermissions() || bleScanner == null) return 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) { private fun handleScanResult(result: ScanResult) {
val device = result.device val device = result.device
val rssi = result.rssi val rssi = result.rssi
val deviceAddress = device.address val deviceAddress = device.address
// Filter out weak signals // Power-aware RSSI filtering
if (rssi < -90) { if (rssi < powerManager.getRSSIThreshold()) {
return return
} }
// FIXED: Check for existing client connections only, allow server connections // Check if already connected or at connection limit
if (gattConnections.containsKey(device)) { if (connectedDevices.containsKey(deviceAddress)) {
Log.d(TAG, "Already have client connection to $deviceAddress")
return 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 currentTime = System.currentTimeMillis()
val pendingTime = pendingConnections[deviceAddress] val existingAttempt = pendingConnections[deviceAddress]
if (pendingTime != null && (currentTime - pendingTime) < connectionRetryDelay) {
Log.d(TAG, "Connection attempt to $deviceAddress already pending, skipping")
return
}
// Check connection attempt limits if (existingAttempt != null) {
val attempts = connectionAttempts.getOrDefault(deviceAddress, 0) if (existingAttempt.isExpired()) {
if (attempts >= maxConnectionAttempts) {
if (pendingTime == null || (currentTime - pendingTime) > connectionRetryDelay * 2) {
// Reset after extended delay
connectionAttempts[deviceAddress] = 0
pendingConnections.remove(deviceAddress) pendingConnections.remove(deviceAddress)
Log.d(TAG, "Reset connection attempts for $deviceAddress after extended delay") } else if (!existingAttempt.shouldRetry()) {
} else {
Log.d(TAG, "Max connection attempts reached for $deviceAddress")
return return
} }
} }
// Store RSSI and track connection attempt // Update connection attempt
peripheralRSSI[deviceAddress] = rssi val attempts = (existingAttempt?.attempts ?: 0) + 1
pendingConnections[deviceAddress] = currentTime pendingConnections[deviceAddress] = ConnectionAttempt(attempts)
connectionAttempts[deviceAddress] = attempts + 1
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 connectToDevice(device, rssi)
connectToDeviceFixed(device)
} }
/**
* Connect to a discovered device - FIXED version with proper timeout and cleanup handling
*/
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
private fun connectToDeviceFixed(device: BluetoothDevice) { private fun connectToDevice(device: BluetoothDevice, rssi: Int) {
if (!hasBluetoothPermissions()) return if (!hasBluetoothPermissions()) return
val deviceAddress = device.address val deviceAddress = device.address
Log.d(TAG, "Attempting to connect to $deviceAddress")
val gattCallback = object : BluetoothGattCallback() { val gattCallback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { 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) { when (newState) {
BluetoothProfile.STATE_CONNECTED -> { BluetoothProfile.STATE_CONNECTED -> {
if (status == BluetoothGatt.GATT_SUCCESS) { if (status == BluetoothGatt.GATT_SUCCESS) {
Log.i(TAG, "Successfully connected to $deviceAddress as client") Log.i(TAG, "Client: Connected to $deviceAddress")
connectedDevices[deviceAddress] = gatt.device val deviceConn = DeviceConnection(
gattConnections[gatt.device] = gatt device = device,
pendingConnections.remove(deviceAddress) // Clear pending connection gatt = gatt,
rssi = rssi,
isClient = true
)
connectedDevices[deviceAddress] = deviceConn
pendingConnections.remove(deviceAddress)
// Discover services with a slight delay for stability
connectionScope.launch { connectionScope.launch {
delay(200) // Small delay for connection stabilization delay(200)
if (gattConnections.containsKey(gatt.device)) { gatt.discoverServices()
gatt.discoverServices()
}
} }
} else { } else {
Log.e(TAG, "Connected to $deviceAddress with error status: $status")
gatt.disconnect() gatt.disconnect()
} }
} }
BluetoothProfile.STATE_DISCONNECTED -> { BluetoothProfile.STATE_DISCONNECTED -> {
Log.d(TAG, "Disconnected from $deviceAddress (status: $status)") Log.d(TAG, "Client: Disconnected from $deviceAddress")
connectedDevices.remove(deviceAddress) cleanupDeviceConnection(deviceAddress)
deviceCharacteristics.remove(gatt.device)
gattConnections.remove(gatt.device)
pendingConnections.remove(deviceAddress) // Clear pending connection
// Handle connection errors connectionScope.launch {
if (status == 147 || status == 133 || status == 8) { // Timeout or connection errors delay(CLEANUP_DELAY)
Log.w(TAG, "Connection failed to $deviceAddress with status $status") try {
} gatt.close()
} catch (e: Exception) {
// Close the GATT connection Log.w(TAG, "Error closing GATT: ${e.message}")
try { }
gatt.close()
} catch (e: Exception) {
Log.e(TAG, "Error closing GATT connection to $deviceAddress: ${e.message}")
} }
} }
} }
} }
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
Log.d(TAG, "onServicesDiscovered: $deviceAddress status=$status")
if (status == BluetoothGatt.GATT_SUCCESS) { if (status == BluetoothGatt.GATT_SUCCESS) {
val service = gatt.getService(SERVICE_UUID) val service = gatt.getService(SERVICE_UUID)
val characteristic = service?.getCharacteristic(CHARACTERISTIC_UUID) val characteristic = service?.getCharacteristic(CHARACTERISTIC_UUID)
if (characteristic != null) { if (characteristic != null) {
Log.d(TAG, "Found bitchat service and characteristic on $deviceAddress") // Update device connection with characteristic
deviceCharacteristics[gatt.device] = characteristic connectedDevices[deviceAddress]?.let { deviceConn ->
val updatedConn = deviceConn.copy(characteristic = characteristic)
connectedDevices[deviceAddress] = updatedConn
}
// Enable notifications // Enable notifications
val notificationSuccess = gatt.setCharacteristicNotification(characteristic, true) gatt.setCharacteristicNotification(characteristic, true)
Log.d(TAG, "Set notification for $deviceAddress: $notificationSuccess")
// Write notification descriptor
val descriptor = characteristic.getDescriptor( val descriptor = characteristic.getDescriptor(
UUID.fromString("00002902-0000-1000-8000-00805f9b34fb") UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
) )
if (descriptor != null) { descriptor?.let {
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE it.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
val descriptorSuccess = gatt.writeDescriptor(descriptor) gatt.writeDescriptor(it)
Log.d(TAG, "Write notification descriptor for $deviceAddress: $descriptorSuccess")
} }
// Notify delegate about successful connection
connectionScope.launch { connectionScope.launch {
delay(100) // Ensure everything is set up delay(100)
delegate?.onDeviceConnected(gatt.device) delegate?.onDeviceConnected(device)
} }
} else { } else {
Log.e(TAG, "Bitchat service or characteristic not found on $deviceAddress")
gatt.disconnect() gatt.disconnect()
} }
} else { } else {
Log.e(TAG, "Service discovery failed on $deviceAddress with status: $status")
gatt.disconnect() gatt.disconnect()
} }
} }
override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
val value = characteristic.value val value = characteristic.value
Log.d(TAG, "Received ${value.size} bytes from $deviceAddress")
val packet = BitchatPacket.fromBinaryData(value) val packet = BitchatPacket.fromBinaryData(value)
if (packet != null) { if (packet != null) {
val peerID = String(packet.senderID).replace("\u0000", "") val peerID = String(packet.senderID).replace("\u0000", "")
delegate?.onPacketReceived(packet, peerID, gatt.device) 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 { try {
val gatt = device.connectGatt(context, false, gattCallback) device.connectGatt(context, false, gattCallback)
if (gatt == null) {
Log.e(TAG, "Failed to create GATT connection to $deviceAddress")
pendingConnections.remove(deviceAddress)
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Exception connecting to $deviceAddress: ${e.message}") Log.e(TAG, "Exception connecting to $deviceAddress: ${e.message}")
pendingConnections.remove(deviceAddress) pendingConnections.remove(deviceAddress)
} }
} }
/** private fun startPeriodicCleanup() {
* Connect to a discovered device - LEGACY METHOD (kept for compatibility) connectionScope.launch {
*/ while (isActive) {
@Suppress("DEPRECATION") delay(CLEANUP_INTERVAL)
private fun connectToDevice(device: BluetoothDevice) {
// Redirect to fixed method if (!isActive) break
connectToDeviceFixed(device)
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() { private fun clearAllConnections() {
connectedDevices.clear() connectedDevices.clear()
deviceCharacteristics.clear()
subscribedDevices.clear() subscribedDevices.clear()
gattConnections.clear()
peripheralRSSI.clear()
// FIXED: Clear new tracking maps
pendingConnections.clear() pendingConnections.clear()
connectionAttempts.clear()
} }
} }
@@ -44,7 +44,7 @@ class BluetoothMeshService(private val context: Context) {
private val securityManager = SecurityManager(encryptionService, myPeerID) private val securityManager = SecurityManager(encryptionService, myPeerID)
private val storeForwardManager = StoreForwardManager() private val storeForwardManager = StoreForwardManager()
private val messageHandler = MessageHandler(myPeerID) 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) private val packetProcessor = PacketProcessor(myPeerID)
// Delegate for message callbacks (maintains same interface) // 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) // MARK: - Command Autocomplete (delegated)
fun updateCommandSuggestions(input: String) { fun updateCommandSuggestions(input: String) {