mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-24 23:25:19 +00:00
Merge pull request #13 from callebtc/ble-fixes
Fix android to android client connections
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>()
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
@@ -55,6 +55,26 @@ class BluetoothMeshService(private val context: Context) {
|
||||
|
||||
init {
|
||||
setupDelegates()
|
||||
startPeriodicDebugLogging()
|
||||
}
|
||||
|
||||
/**
|
||||
* Start periodic debug logging every 10 seconds
|
||||
*/
|
||||
private fun startPeriodicDebugLogging() {
|
||||
serviceScope.launch {
|
||||
while (isActive) {
|
||||
try {
|
||||
delay(10000) // 10 seconds
|
||||
val debugInfo = getDebugStatus()
|
||||
Log.d(TAG, "=== PERIODIC DEBUG STATUS ===")
|
||||
Log.d(TAG, debugInfo)
|
||||
Log.d(TAG, "=== END DEBUG STATUS ===")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error in periodic debug logging: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -330,10 +350,7 @@ class BluetoothMeshService(private val context: Context) {
|
||||
)
|
||||
|
||||
// Send with random delay and retry for reliability
|
||||
delay(Random.nextLong(50, 500))
|
||||
connectionManager.broadcastPacket(packet)
|
||||
|
||||
delay(300 + Random.nextLong(0, 200))
|
||||
// delay(Random.nextLong(50, 500))
|
||||
connectionManager.broadcastPacket(packet)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,334 @@
|
||||
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 {
|
||||
// CRITICAL FIX: Set reportDelay to 0 for all modes.
|
||||
// When using a custom duty cycle, we want scan results delivered immediately,
|
||||
// not batched. A non-zero report delay can conflict with the scan window,
|
||||
// causing missed results if the scan stops before the delay is met.
|
||||
val builder = ScanSettings.Builder()
|
||||
.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
|
||||
|
||||
when (currentMode) {
|
||||
PowerMode.PERFORMANCE -> builder
|
||||
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
|
||||
.setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
|
||||
.setNumOfMatches(ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT)
|
||||
|
||||
PowerMode.BALANCED -> builder
|
||||
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
|
||||
.setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
|
||||
.setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT)
|
||||
|
||||
PowerMode.POWER_SAVER -> builder
|
||||
.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
|
||||
.setMatchMode(ScanSettings.MATCH_MODE_STICKY)
|
||||
.setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT)
|
||||
|
||||
PowerMode.ULTRA_LOW_POWER -> builder
|
||||
.setScanMode(ScanSettings.SCAN_MODE_OPPORTUNISTIC)
|
||||
.setMatchMode(ScanSettings.MATCH_MODE_STICKY)
|
||||
.setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT)
|
||||
}
|
||||
|
||||
return builder.setReportDelay(0).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