diff --git a/bitchat/Info.plist b/bitchat/Info.plist index 37b0d941..9f76f40c 100644 --- a/bitchat/Info.plist +++ b/bitchat/Info.plist @@ -40,6 +40,7 @@ bluetooth-central bluetooth-peripheral remote-notification + background-processing UILaunchStoryboardName LaunchScreen diff --git a/bitchat/Services/BluetoothMeshService.swift b/bitchat/Services/BluetoothMeshService.swift index 6e1db1a6..00719859 100644 --- a/bitchat/Services/BluetoothMeshService.swift +++ b/bitchat/Services/BluetoothMeshService.swift @@ -601,6 +601,17 @@ class BluetoothMeshService: NSObject { // App state tracking private var isAppInForeground = true + // Background task management + #if os(iOS) + private var backgroundTask: UIBackgroundTaskIdentifier = .invalid + private var scanBackgroundTask: UIBackgroundTaskIdentifier = .invalid + private var advertiseBackgroundTask: UIBackgroundTaskIdentifier = .invalid + #endif + + // Core Bluetooth state restoration identifiers + private static let centralManagerRestorationID = "com.bitchat.central" + private static let peripheralManagerRestorationID = "com.bitchat.peripheral" + // Battery optimizer integration private let batteryOptimizer = BatteryOptimizer.shared private var batteryOptimizerCancellables = Set() @@ -1498,8 +1509,17 @@ class BluetoothMeshService: NSObject { // Set up queue-specific key for deadlock prevention collectionsQueue.setSpecific(key: collectionsQueueKey, value: ()) - centralManager = CBCentralManager(delegate: self, queue: nil) - peripheralManager = CBPeripheralManager(delegate: self, queue: nil) + // Initialize Bluetooth managers with state restoration + centralManager = CBCentralManager( + delegate: self, + queue: nil, + options: [CBCentralManagerOptionRestoreIdentifierKey: Self.centralManagerRestorationID] + ) + peripheralManager = CBPeripheralManager( + delegate: self, + queue: nil, + options: [CBPeripheralManagerOptionRestoreIdentifierKey: Self.peripheralManagerRestorationID] + ) // Setup app state notifications setupAppStateNotifications() @@ -1691,6 +1711,52 @@ class BluetoothMeshService: NSObject { cleanup() } + // MARK: - Background Task Management + + #if os(iOS) + private func beginBackgroundTask() -> UIBackgroundTaskIdentifier { + return UIApplication.shared.beginBackgroundTask { [weak self] in + self?.endBackgroundTask(self?.backgroundTask ?? .invalid) + } + } + + private func endBackgroundTask(_ taskID: UIBackgroundTaskIdentifier) { + if taskID != .invalid { + UIApplication.shared.endBackgroundTask(taskID) + } + } + + private func beginScanBackgroundTask() { + guard scanBackgroundTask == .invalid else { return } + + scanBackgroundTask = UIApplication.shared.beginBackgroundTask(withName: "BluetoothScan") { [weak self] in + self?.endScanBackgroundTask() + } + } + + private func endScanBackgroundTask() { + if scanBackgroundTask != .invalid { + UIApplication.shared.endBackgroundTask(scanBackgroundTask) + scanBackgroundTask = .invalid + } + } + + private func beginAdvertiseBackgroundTask() { + guard advertiseBackgroundTask == .invalid else { return } + + advertiseBackgroundTask = UIApplication.shared.beginBackgroundTask(withName: "BluetoothAdvertise") { [weak self] in + self?.endAdvertiseBackgroundTask() + } + } + + private func endAdvertiseBackgroundTask() { + if advertiseBackgroundTask != .invalid { + UIApplication.shared.endBackgroundTask(advertiseBackgroundTask) + advertiseBackgroundTask = .invalid + } + } + #endif + // MARK: - App State Management private func setupAppStateNotifications() { @@ -1707,6 +1773,18 @@ class BluetoothMeshService: NSObject { name: UIApplication.willResignActiveNotification, object: nil ) + NotificationCenter.default.addObserver( + self, + selector: #selector(appDidEnterBackground), + name: UIApplication.didEnterBackgroundNotification, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(appWillEnterForeground), + name: UIApplication.willEnterForegroundNotification, + object: nil + ) #elseif os(macOS) NotificationCenter.default.addObserver( self, @@ -1733,6 +1811,40 @@ class BluetoothMeshService: NSObject { // App will resign active } + @objc private func appDidEnterBackground() { + isAppInForeground = false + SecureLogger.log("📱 App entered background - switching to background scanning mode", level: .info) + + #if os(iOS) + // Begin background tasks for critical operations + beginScanBackgroundTask() + beginAdvertiseBackgroundTask() + #endif + + // Switch to background scanning mode (continuous with battery-efficient options) + switchToBackgroundScanning() + + // Update advertising for background mode + updateAdvertisingForBackgroundMode() + } + + @objc private func appWillEnterForeground() { + isAppInForeground = true + SecureLogger.log("📱 App entering foreground - switching to foreground scanning mode", level: .info) + + #if os(iOS) + // End background tasks as app is coming to foreground + endScanBackgroundTask() + endAdvertiseBackgroundTask() + #endif + + // Switch to foreground scanning mode (with duty cycling) + switchToForegroundScanning() + + // Update advertising for foreground mode + updateAdvertisingForForegroundMode() + } + private func cleanup() { // Send leave announcement before disconnecting sendLeaveAnnouncement() @@ -1973,14 +2085,41 @@ class BluetoothMeshService: NSObject { peripheralManager?.startAdvertising(advertisementData) } + private func updateAdvertisingForBackgroundMode() { + // Check if we should skip advertising in critically low battery + if batteryOptimizer.currentPowerMode == .ultraLowPower && batteryOptimizer.batteryLevel < 0.1 { + // Only skip advertising if battery is critically low (<10%) + if isAdvertising { + peripheralManager?.stopAdvertising() + isAdvertising = false + SecureLogger.log("⚡ Stopped advertising due to critically low battery", level: .info) + } + return + } + + // Continue advertising in background mode with battery considerations + if !isAdvertising && peripheralManager?.state == .poweredOn { + startAdvertising() + SecureLogger.log("📡 Continued advertising in background mode", level: .info) + } + } + + private func updateAdvertisingForForegroundMode() { + // Always start advertising when in foreground if possible + if !isAdvertising && peripheralManager?.state == .poweredOn { + startAdvertising() + SecureLogger.log("📡 Started advertising in foreground mode", level: .info) + } + } + func startScanning() { guard centralManager?.state == .poweredOn else { return } - // Enable duplicate detection + // Optimize scan options based on foreground/background state let scanOptions: [String: Any] = [ - CBCentralManagerScanOptionAllowDuplicatesKey: true + CBCentralManagerScanOptionAllowDuplicatesKey: isAppInForeground ? true : false ] centralManager?.scanForPeripherals( @@ -1991,8 +2130,10 @@ class BluetoothMeshService: NSObject { // Update scan parameters based on battery before starting updateScanParametersForBattery() - // Implement scan duty cycling for battery efficiency - scheduleScanDutyCycle() + // Implement scan duty cycling for battery efficiency (only in foreground) + if isAppInForeground { + scheduleScanDutyCycle() + } } func triggerRescan() { @@ -2061,8 +2202,18 @@ class BluetoothMeshService: NSObject { func sendMessage(_ content: String, mentions: [String] = [], to recipientID: String? = nil, messageID: String? = nil, timestamp: Date? = nil) { // Defensive check for empty content guard !content.isEmpty else { return } + + #if os(iOS) + let backgroundTask = beginBackgroundTask() + #endif + messageQueue.async { [weak self] in - guard let self = self else { return } + guard let self = self else { + #if os(iOS) + self?.endBackgroundTask(backgroundTask) + #endif + return + } let nickname = self.delegate as? ChatViewModel let senderNick = nickname?.nickname ?? self.myPeerID @@ -2112,7 +2263,14 @@ class BluetoothMeshService: NSObject { let initialDelay = self.smartCollisionAvoidanceDelay(baseDelay: self.randomDelay()) DispatchQueue.main.asyncAfter(deadline: .now() + initialDelay) { [weak self] in self?.broadcastPacket(packet) + #if os(iOS) + self?.endBackgroundTask(backgroundTask) + #endif } + } else { + #if os(iOS) + self.endBackgroundTask(backgroundTask) + #endif } } } @@ -2134,8 +2292,17 @@ class BluetoothMeshService: NSObject { let msgID = messageID ?? UUID().uuidString + #if os(iOS) + let backgroundTask = beginBackgroundTask() + #endif + messageQueue.async { [weak self] in - guard let self = self else { return } + guard let self = self else { + #if os(iOS) + self?.endBackgroundTask(backgroundTask) + #endif + return + } // Check if this is an old peer ID that has rotated var targetPeerID = recipientPeerID @@ -2150,6 +2317,10 @@ class BluetoothMeshService: NSObject { // Always use Noise encryption self.sendPrivateMessageViaNoise(content, to: targetPeerID, recipientNickname: recipientNickname, messageID: msgID) + + #if os(iOS) + self.endBackgroundTask(backgroundTask) + #endif } } @@ -4796,6 +4967,50 @@ extension BluetoothMeshService: CBCentralManagerDelegate { } } + func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) { + // Restore central manager state after app backgrounding + SecureLogger.log("🔄 Restoring CBCentralManager state", level: .info) + + // Restore scanned services + if let services = dict[CBCentralManagerRestoredStateScanServicesKey] as? [CBUUID] { + SecureLogger.log("📡 Restoring scanned services: \(services)", level: .info) + } + + // Restore scan options + if let scanOptions = dict[CBCentralManagerRestoredStateScanOptionsKey] as? [String: Any] { + SecureLogger.log("🔍 Restoring scan options: \(scanOptions)", level: .info) + } + + // Restore peripherals + if let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral] { + SecureLogger.log("📱 Restoring \(peripherals.count) peripherals", level: .info) + + collectionsQueue.async(flags: .barrier) { [weak self] in + guard let self = self else { return } + + // Restore discovered peripherals list + self.discoveredPeripherals = peripherals + + // Restore connections for connected peripherals + for peripheral in peripherals { + if peripheral.state == .connected { + // Find the peerID for this peripheral + let peerID = peripheral.identifier.uuidString + + // Update our tracking + self.updatePeripheralConnection(peerID, peripheral: peripheral) + + // Set delegate and discover services + peripheral.delegate = self + peripheral.discoverServices([BluetoothMeshService.serviceUUID]) + + SecureLogger.log("🔗 Restored connection to peer: \(peerID)", level: .info) + } + } + } + } + } + func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { let peripheralID = peripheral.identifier.uuidString @@ -5311,6 +5526,33 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate { } } + func peripheralManager(_ peripheral: CBPeripheralManager, willRestoreState dict: [String : Any]) { + // Restore peripheral manager state after app backgrounding + SecureLogger.log("🔄 Restoring CBPeripheralManager state", level: .info) + + // Restore services + if let services = dict[CBPeripheralManagerRestoredStateServicesKey] as? [CBMutableService] { + SecureLogger.log("🛠 Restoring \(services.count) services", level: .info) + + // Services are automatically restored by Core Bluetooth + // We just need to ensure our internal state is consistent + DispatchQueue.main.async { [weak self] in + self?.isAdvertising = false + // Will be set to true when advertising starts + } + } + + // Restore advertisement data + if let advertisementData = dict[CBPeripheralManagerRestoredStateAdvertisementDataKey] as? [String: Any] { + SecureLogger.log("📣 Restoring advertisement data: \(advertisementData)", level: .info) + self.advertisementData = advertisementData + + DispatchQueue.main.async { [weak self] in + self?.isAdvertising = true + } + } + } + func peripheralManager(_ peripheral: CBPeripheralManager, didAdd service: CBService, error: Error?) { // Service added } @@ -5503,6 +5745,51 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate { // Keeping empty implementation for compatibility } + private func switchToBackgroundScanning() { + guard centralManager?.state == .poweredOn else { return } + + // Stop existing scanning and duty cycling + centralManager?.stopScan() + scanDutyCycleTimer?.invalidate() + scanDutyCycleTimer = nil + + // Use continuous scanning with battery-efficient options in background + let backgroundScanOptions: [String: Any] = [ + CBCentralManagerScanOptionAllowDuplicatesKey: false // Battery efficient + ] + + centralManager?.scanForPeripherals( + withServices: [BluetoothMeshService.serviceUUID], + options: backgroundScanOptions + ) + + SecureLogger.log("🔄 Switched to continuous background scanning", level: .info) + } + + private func switchToForegroundScanning() { + guard centralManager?.state == .poweredOn else { return } + + // Stop existing scanning + centralManager?.stopScan() + scanDutyCycleTimer?.invalidate() + scanDutyCycleTimer = nil + + // Use foreground scanning with duty cycling and allow duplicates for faster discovery + let foregroundScanOptions: [String: Any] = [ + CBCentralManagerScanOptionAllowDuplicatesKey: true // Faster discovery + ] + + centralManager?.scanForPeripherals( + withServices: [BluetoothMeshService.serviceUUID], + options: foregroundScanOptions + ) + + // Re-enable duty cycling for foreground operation + scheduleScanDutyCycle() + + SecureLogger.log("🔄 Switched to foreground scanning with duty cycling", level: .info) + } + private func updateScanParametersForBattery() { // This method is now handled by BatteryOptimizer through handlePowerModeChange // Keeping empty implementation for compatibility diff --git a/bitchat/Utils/BatteryOptimizer.swift b/bitchat/Utils/BatteryOptimizer.swift index d520012b..e88c779f 100644 --- a/bitchat/Utils/BatteryOptimizer.swift +++ b/bitchat/Utils/BatteryOptimizer.swift @@ -184,13 +184,15 @@ class BatteryOptimizer { // When charging, use performance mode unless battery is critical currentPowerMode = batteryLevel < 0.1 ? .balanced : .performance } else if isInBackground { - // In background, always use power saving - if batteryLevel < 0.2 { + // In background, be less aggressive with power management + // Don't force ultra-low power mode until battery is below 10% + // Use balanced mode in background above 30% battery + if batteryLevel < 0.1 { currentPowerMode = .ultraLowPower - } else if batteryLevel < 0.5 { + } else if batteryLevel < 0.3 { currentPowerMode = .powerSaver } else { - currentPowerMode = .balanced + currentPowerMode = .balanced // Use balanced mode above 30% in background } } else { // Foreground, not charging