mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-24 23:25:19 +00:00
power saving
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user