diff --git a/apps/ios/Shared/AppDelegate.swift b/apps/ios/Shared/AppDelegate.swift index bb1de94359..145e362797 100644 --- a/apps/ios/Shared/AppDelegate.swift +++ b/apps/ios/Shared/AppDelegate.swift @@ -81,7 +81,7 @@ class AppDelegate: NSObject, UIApplicationDelegate { } } else if let checkMessages = ntfData["checkMessages"] as? Bool, checkMessages { logger.debug("AppDelegate: didReceiveRemoteNotification: checkMessages") - if m.ntfEnablePeriodic && allowBackgroundRefresh() { + if m.ntfEnablePeriodic && allowBackgroundRefresh() && BGManager.shared.lastRanLongAgo { receiveMessages(completionHandler) } else { completionHandler(.noData) diff --git a/apps/ios/Shared/Model/BGManager.swift b/apps/ios/Shared/Model/BGManager.swift index a39155efe8..25eab6c69e 100644 --- a/apps/ios/Shared/Model/BGManager.swift +++ b/apps/ios/Shared/Model/BGManager.swift @@ -16,7 +16,12 @@ private let receiveTaskId = "chat.simplex.app.receive" private let waitForMessages: TimeInterval = 6 // This is the smallest interval between refreshes, and also target interval in "off" mode -private let bgRefreshInterval: TimeInterval = 600 +private let bgRefreshInterval: TimeInterval = 600 // 10 minutes + +// This intervals are used for background refresh in instant and periodic modes +private let periodicBgRefreshInterval: TimeInterval = 1200 // 20 minutes + +private let maxBgRefreshInterval: TimeInterval = 2400 // 40 minutes private let maxTimerCount = 9 @@ -34,14 +39,14 @@ class BGManager { } } - func schedule() { + func schedule(interval: TimeInterval? = nil) { if !ChatModel.shared.ntfEnableLocal { logger.debug("BGManager.schedule: disabled") return } logger.debug("BGManager.schedule") let request = BGAppRefreshTaskRequest(identifier: receiveTaskId) - request.earliestBeginDate = Date(timeIntervalSinceNow: bgRefreshInterval) + request.earliestBeginDate = Date(timeIntervalSinceNow: interval ?? runInterval) do { try BGTaskScheduler.shared.submit(request) } catch { @@ -49,20 +54,34 @@ class BGManager { } } + var runInterval: TimeInterval { + switch ChatModel.shared.notificationMode { + case .instant: maxBgRefreshInterval + case .periodic: periodicBgRefreshInterval + case .off: bgRefreshInterval + } + } + + var lastRanLongAgo: Bool { + Date.now.timeIntervalSince(chatLastBackgroundRunGroupDefault.get()) > runInterval + } + private func handleRefresh(_ task: BGAppRefreshTask) { if !ChatModel.shared.ntfEnableLocal { logger.debug("BGManager.handleRefresh: disabled") return } logger.debug("BGManager.handleRefresh") - schedule() - if allowBackgroundRefresh() { + let shouldRun_ = lastRanLongAgo + if allowBackgroundRefresh() && shouldRun_ { + schedule() let completeRefresh = completionHandler { task.setTaskCompleted(success: true) } task.expirationHandler = { completeRefresh("expirationHandler") } receiveMessages(completeRefresh) } else { + schedule(interval: shouldRun_ ? bgRefreshInterval : runInterval) logger.debug("BGManager.completionHandler: already active, not started") task.setTaskCompleted(success: true) } @@ -91,6 +110,7 @@ class BGManager { } self.completed = false DispatchQueue.main.async { + chatLastBackgroundRunGroupDefault.set(Date.now) let m = ChatModel.shared if (!m.chatInitialized) { setAppState(.bgRefresh) diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift index eaa1131eb8..c286ee1c3c 100644 --- a/apps/ios/SimpleX NSE/NotificationService.swift +++ b/apps/ios/SimpleX NSE/NotificationService.swift @@ -14,9 +14,11 @@ import SimpleXChat let logger = Logger() -let suspendingDelay: UInt64 = 2_500_000_000 +let appSuspendingDelay: UInt64 = 2_500_000_000 -let nseSuspendTimeout: Int = 10 +let nseSuspendDelay: TimeInterval = 2 + +let nseSuspendTimeout: Int = 5 typealias NtfStream = ConcurrentQueue @@ -177,6 +179,10 @@ class NSEThreads { return false } } + + var noThreads: Bool { + allThreads.isEmpty + } } // Notification service extension creates a new instance of the class and calls didReceive for each notification. @@ -261,7 +267,7 @@ class NotificationService: UNNotificationServiceExtension { let dbStatus = startChat() if case .ok = dbStatus, let ntfInfo = apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo) { - logger.debug("NotificationService: receiveNtfMessages: apiGetNtfMessage \(String(describing: ntfInfo), privacy: .public)") + logger.debug("NotificationService: receiveNtfMessages: apiGetNtfMessage \(String(describing: ntfInfo.ntfMessages.count), privacy: .public)") if let connEntity = ntfInfo.connEntity_ { setBestAttemptNtf( ntfInfo.ntfsEnabled @@ -326,7 +332,15 @@ class NotificationService: UNNotificationServiceExtension { if let t = threadId { threadId = nil if NSEThreads.shared.endThread(t) { - suspendChat(nseSuspendTimeout) + logger.debug("NotificationService.deliverBestAttemptNtf: will suspend") + // suspension is delayed to allow chat core finalise any processing + // (e.g., send delivery receipts) + DispatchQueue.global().asyncAfter(deadline: .now() + nseSuspendDelay) { + if NSEThreads.shared.noThreads { + logger.debug("NotificationService.deliverBestAttemptNtf: suspending...") + suspendChat(nseSuspendTimeout) + } + } } } if let handler = contentHandler, let ntf = bestAttemptNtf { @@ -497,7 +511,7 @@ func suspendChat(_ timeout: Int) { NSEChatState.shared.set(.suspending) if apiSuspendChat(timeoutMicroseconds: timeout * 1000000) { - logger.debug("NotificationService: activateChat: after apiActivateChat") + logger.debug("NotificationService: suspendChat: after apiSuspendChat") DispatchQueue.global().asyncAfter(deadline: .now() + Double(timeout) + 1, execute: chatSuspended) } else { NSEChatState.shared.set(state) @@ -510,6 +524,7 @@ func chatSuspended() { if case .suspending = NSEChatState.shared.value { NSEChatState.shared.set(.suspended) chatCloseStore() + logger.debug("NotificationService chatSuspended: suspended") } } diff --git a/apps/ios/SimpleXChat/AppGroup.swift b/apps/ios/SimpleXChat/AppGroup.swift index 10625e2edf..f79c294e0c 100644 --- a/apps/ios/SimpleXChat/AppGroup.swift +++ b/apps/ios/SimpleXChat/AppGroup.swift @@ -15,6 +15,7 @@ let GROUP_DEFAULT_APP_STATE = "appState" let GROUP_DEFAULT_NSE_STATE = "nseState" let GROUP_DEFAULT_DB_CONTAINER = "dbContainer" public let GROUP_DEFAULT_CHAT_LAST_START = "chatLastStart" +public let GROUP_DEFAULT_CHAT_LAST_BACKGROUND_RUN = "chatLastBackgroundRun" let GROUP_DEFAULT_NTF_PREVIEW_MODE = "ntfPreviewMode" public let GROUP_DEFAULT_NTF_ENABLE_LOCAL = "ntfEnableLocal" // no longer used public let GROUP_DEFAULT_NTF_ENABLE_PERIODIC = "ntfEnablePeriodic" // no longer used @@ -156,6 +157,8 @@ public let dbContainerGroupDefault = EnumDefault( public let chatLastStartGroupDefault = DateDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_CHAT_LAST_START) +public let chatLastBackgroundRunGroupDefault = DateDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_CHAT_LAST_BACKGROUND_RUN) + public let ntfPreviewModeGroupDefault = EnumDefault( defaults: groupDefaults, forKey: GROUP_DEFAULT_NTF_PREVIEW_MODE,