mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2026-03-31 11:03:28 +00:00
1172 lines
56 KiB
Swift
1172 lines
56 KiB
Swift
import Foundation
|
|
import BackgroundTasks
|
|
import AVFAudio
|
|
import UIKit
|
|
import SwiftSignalKit
|
|
import Postbox
|
|
import TelegramCore
|
|
import TelegramCallsUI
|
|
import AccountContext
|
|
import UniversalMediaPlayer
|
|
import TelegramAudio
|
|
import TelegramPresentationData
|
|
|
|
private struct AccountTasks {
|
|
let stateSynchronization: Bool
|
|
let importantTasks: AccountRunningImportantTasks
|
|
let backgroundLocation: Bool
|
|
let backgroundDownloads: Bool
|
|
let backgroundAudio: Bool
|
|
let activeCalls: Bool
|
|
let watchTasks: Bool
|
|
let userInterfaceInUse: Bool
|
|
|
|
var isEmpty: Bool {
|
|
if self.stateSynchronization {
|
|
return false
|
|
}
|
|
if !self.importantTasks.isEmpty {
|
|
return false
|
|
}
|
|
if self.backgroundLocation {
|
|
return false
|
|
}
|
|
if self.backgroundDownloads {
|
|
return false
|
|
}
|
|
if self.backgroundAudio {
|
|
return false
|
|
}
|
|
if self.activeCalls {
|
|
return false
|
|
}
|
|
if self.watchTasks {
|
|
return false
|
|
}
|
|
if self.userInterfaceInUse {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
private struct PendingMediaUploadKey: Hashable {
|
|
let accountId: AccountRecordId
|
|
let messageId: MessageId
|
|
}
|
|
|
|
private struct PendingStoryUploadKey: Hashable {
|
|
let accountId: AccountRecordId
|
|
let stableId: Int32
|
|
}
|
|
|
|
public final class SharedWakeupManager {
|
|
private let beginBackgroundTask: (String, @escaping () -> Void) -> UIBackgroundTaskIdentifier?
|
|
private let endBackgroundTask: (UIBackgroundTaskIdentifier) -> Void
|
|
private let backgroundTimeRemaining: () -> Double
|
|
private let acquireIdleExtension: () -> Disposable?
|
|
|
|
private var enableBackgroundTasks: Bool = false
|
|
private let presentationData: () -> PresentationData?
|
|
|
|
private var inForeground: Bool = false
|
|
private var hasActiveAudioSession: Bool = false
|
|
private var activeExplicitExtensionTimer: SwiftSignalKit.Timer?
|
|
private var activeExplicitExtensionTask: UIBackgroundTaskIdentifier?
|
|
private var allowBackgroundTimeExtensionDeadline: Double?
|
|
private var allowBackgroundTimeExtensionDeadlineTimer: SwiftSignalKit.Timer?
|
|
private var isInBackgroundExtension: Bool = false
|
|
|
|
private var accountSettingsDisposable: Disposable?
|
|
private var inForegroundDisposable: Disposable?
|
|
private var hasActiveAudioSessionDisposable: Disposable?
|
|
private var tasksDisposable: Disposable?
|
|
private var pendingMediaUploadsDisposable: Disposable?
|
|
private var pendingStoryUploadsDisposable: Disposable?
|
|
private var currentTask: (UIBackgroundTaskIdentifier, Double, SwiftSignalKit.Timer)?
|
|
private var currentExternalCompletion: (() -> Void, SwiftSignalKit.Timer)?
|
|
private var currentExternalCompletionValidationTimer: SwiftSignalKit.Timer?
|
|
|
|
private var managedPausedInBackgroundPlayer: Disposable?
|
|
private var keepIdleDisposable: Disposable?
|
|
private var silenceAudioRenderer: MediaPlayerAudioRenderer?
|
|
|
|
private var accountsAndTasks: [(Account, Bool, AccountTasks)] = []
|
|
|
|
private var pendingMediaUploadsByKey: [PendingMediaUploadKey: Float] = [:]
|
|
private var backgroundProcessingTaskProgressByKey: [PendingMediaUploadKey: Float] = [:]
|
|
private var nextBackgroundProcessingTaskId: Int = 0
|
|
private var backgroundProcessingTaskId: String?
|
|
private var backgroundProcessingTaskLaunched: Bool = false
|
|
private var backgroundProcessingTaskCancellationRequestedByApp: Bool = false
|
|
|
|
private var pendingStoryUploadsByKey: [PendingStoryUploadKey: Float] = [:]
|
|
private var pendingStoryUploadStatusesByKey: [PendingStoryUploadKey: PendingStoryUploadStatus] = [:]
|
|
private var backgroundStoryProcessingTaskProgressByKey: [PendingStoryUploadKey: Float] = [:]
|
|
private var nextBackgroundStoryProcessingTaskId: Int = 0
|
|
private var backgroundStoryProcessingTaskId: String?
|
|
private var backgroundStoryProcessingTaskLaunched: Bool = false
|
|
private var backgroundStoryProcessingTaskCancellationRequestedByApp: Bool = false
|
|
|
|
public init(beginBackgroundTask: @escaping (String, @escaping () -> Void) -> UIBackgroundTaskIdentifier?, endBackgroundTask: @escaping (UIBackgroundTaskIdentifier) -> Void, backgroundTimeRemaining: @escaping () -> Double, acquireIdleExtension: @escaping () -> Disposable?, activeAccounts: Signal<(primary: Account?, accounts: [(AccountRecordId, Account)]), NoError>, liveLocationPolling: Signal<AccountRecordId?, NoError>, watchTasks: Signal<AccountRecordId?, NoError>, inForeground: Signal<Bool, NoError>, hasActiveAudioSession: Signal<Bool, NoError>, notificationManager: SharedNotificationManager?, mediaManager: MediaManager, callManager: PresentationCallManager?, accountUserInterfaceInUse: @escaping (AccountRecordId) -> Signal<Bool, NoError>, presentationData: @escaping () -> PresentationData?) {
|
|
assert(Queue.mainQueue().isCurrent())
|
|
|
|
self.beginBackgroundTask = beginBackgroundTask
|
|
self.endBackgroundTask = endBackgroundTask
|
|
self.backgroundTimeRemaining = backgroundTimeRemaining
|
|
self.acquireIdleExtension = acquireIdleExtension
|
|
self.presentationData = presentationData
|
|
|
|
self.accountSettingsDisposable = (activeAccounts
|
|
|> mapToSignal { activeAccounts -> Signal<Bool, NoError> in
|
|
guard let account = activeAccounts.primary else {
|
|
return .single(false)
|
|
}
|
|
return account.postbox.transaction { transaction -> Bool in
|
|
guard let data = currentAppConfiguration(transaction: transaction).data else {
|
|
return false
|
|
}
|
|
if data["ios_killswitch_disable_bgtasks"] != nil {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|> deliverOnMainQueue
|
|
|> distinctUntilChanged).startStrict(next: { [weak self] isEnabled in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.enableBackgroundTasks = isEnabled
|
|
})
|
|
|
|
self.inForegroundDisposable = (inForeground
|
|
|> deliverOnMainQueue).startStrict(next: { [weak self] value in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.inForeground = value
|
|
if value {
|
|
strongSelf.activeExplicitExtensionTimer?.invalidate()
|
|
strongSelf.activeExplicitExtensionTimer = nil
|
|
if let activeExplicitExtensionTask = strongSelf.activeExplicitExtensionTask {
|
|
strongSelf.activeExplicitExtensionTask = nil
|
|
strongSelf.endBackgroundTask(activeExplicitExtensionTask)
|
|
}
|
|
strongSelf.allowBackgroundTimeExtensionDeadlineTimer?.invalidate()
|
|
strongSelf.allowBackgroundTimeExtensionDeadlineTimer = nil
|
|
}
|
|
strongSelf.updateBackgroundProcessingTaskStateFromPendingMediaUploads()
|
|
strongSelf.updateBackgroundProcessingTaskStateFromPendingStoryUploads()
|
|
strongSelf.checkTasks()
|
|
})
|
|
|
|
self.hasActiveAudioSessionDisposable = (hasActiveAudioSession
|
|
|> deliverOnMainQueue).startStrict(next: { [weak self] value in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.hasActiveAudioSession = value
|
|
strongSelf.checkTasks()
|
|
})
|
|
|
|
self.managedPausedInBackgroundPlayer = combineLatest(queue: .mainQueue(), mediaManager.activeGlobalMediaPlayerAccountId, inForeground).startStrict(next: { [weak mediaManager] accountAndActive, inForeground in
|
|
guard let mediaManager = mediaManager else {
|
|
return
|
|
}
|
|
if !inForeground, let accountAndActive = accountAndActive, !accountAndActive.1 {
|
|
mediaManager.audioSession.dropAll()
|
|
}
|
|
})
|
|
|
|
self.tasksDisposable = (activeAccounts
|
|
|> deliverOnMainQueue
|
|
|> mapToSignal { primary, accounts -> Signal<[(Account, Bool, AccountTasks)], NoError> in
|
|
let signals: [Signal<(Account, Bool, AccountTasks), NoError>] = accounts.map { _, account in
|
|
let hasActiveMedia = mediaManager.activeGlobalMediaPlayerAccountId
|
|
|> map { idAndStatus -> Bool in
|
|
if let (id, isPlaying) = idAndStatus {
|
|
return id == account.id && isPlaying
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|> distinctUntilChanged
|
|
let isPlayingBackgroundAudio = combineLatest(queue: .mainQueue(), hasActiveMedia, hasActiveAudioSession)
|
|
|> map { hasActiveMedia, hasActiveAudioSession -> Bool in
|
|
return hasActiveMedia && hasActiveAudioSession
|
|
}
|
|
|> distinctUntilChanged
|
|
|
|
let hasActiveCalls = (callManager?.currentCallSignal ?? .single(nil))
|
|
|> map { call in
|
|
return call?.context.account.id == account.id
|
|
}
|
|
|> distinctUntilChanged
|
|
|
|
let hasActiveGroupCalls = (callManager?.currentGroupCallSignal ?? .single(nil))
|
|
|> map { call -> Bool in
|
|
guard let call else {
|
|
return false
|
|
}
|
|
switch call {
|
|
case let .conferenceSource(conferenceSource):
|
|
return conferenceSource.context.account.id == account.id
|
|
case let .group(groupCall):
|
|
return groupCall.accountContext.account.id == account.id
|
|
}
|
|
}
|
|
|> distinctUntilChanged
|
|
|
|
let keepUpdatesForCalls = combineLatest(queue: .mainQueue(), hasActiveCalls, hasActiveGroupCalls)
|
|
|> map { hasActiveCalls, hasActiveGroupCalls -> Bool in
|
|
return hasActiveCalls || hasActiveGroupCalls
|
|
}
|
|
|> distinctUntilChanged
|
|
|
|
let isPlayingBackgroundActiveCall = combineLatest(queue: .mainQueue(), hasActiveCalls, hasActiveGroupCalls, hasActiveAudioSession)
|
|
|> map { hasActiveCalls, hasActiveGroupCalls, hasActiveAudioSession -> Bool in
|
|
return (hasActiveCalls || hasActiveGroupCalls) && hasActiveAudioSession
|
|
}
|
|
|> distinctUntilChanged
|
|
|
|
let hasActiveAudio = combineLatest(queue: .mainQueue(), isPlayingBackgroundAudio, isPlayingBackgroundActiveCall)
|
|
|> map { isPlayingBackgroundAudio, isPlayingBackgroundActiveCall in
|
|
return isPlayingBackgroundAudio || isPlayingBackgroundActiveCall
|
|
}
|
|
|> distinctUntilChanged
|
|
|
|
let hasActiveLiveLocationPolling = liveLocationPolling
|
|
|> map { id in
|
|
return id == account.id
|
|
}
|
|
|> distinctUntilChanged
|
|
|
|
let hasWatchTasks = watchTasks
|
|
|> map { id in
|
|
return id == account.id
|
|
}
|
|
|> distinctUntilChanged
|
|
|
|
let userInterfaceInUse = accountUserInterfaceInUse(account.id)
|
|
|
|
return combineLatest(queue: .mainQueue(), account.importantTasksRunning, notificationManager?.isPollingState(accountId: account.id) ?? .single(false), hasActiveAudio, keepUpdatesForCalls, hasActiveLiveLocationPolling, hasWatchTasks, userInterfaceInUse)
|
|
|> map { importantTasksRunning, isPollingState, hasActiveAudio, keepUpdatesForCalls, hasActiveLiveLocationPolling, hasWatchTasks, userInterfaceInUse -> (Account, Bool, AccountTasks) in
|
|
return (account, primary?.id == account.id, AccountTasks(stateSynchronization: isPollingState, importantTasks: importantTasksRunning, backgroundLocation: hasActiveLiveLocationPolling, backgroundDownloads: false, backgroundAudio: hasActiveAudio, activeCalls: keepUpdatesForCalls, watchTasks: hasWatchTasks, userInterfaceInUse: userInterfaceInUse))
|
|
}
|
|
}
|
|
return combineLatest(signals)
|
|
}
|
|
|> deliverOnMainQueue).startStrict(next: { [weak self] accountsAndTasks in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.accountsAndTasks = accountsAndTasks
|
|
strongSelf.checkTasks()
|
|
})
|
|
|
|
self.pendingMediaUploadsDisposable = (activeAccounts
|
|
|> deliverOnMainQueue
|
|
|> mapToSignal { _, accounts -> Signal<[PendingMediaUploadKey: Float], NoError> in
|
|
if accounts.isEmpty {
|
|
return .single([:])
|
|
}
|
|
let signals: [Signal<[PendingMediaUploadKey: Float], NoError>] = accounts.map { accountId, account in
|
|
return account.pendingMessageManager.pendingMediaUploads
|
|
|> map { pendingMediaUploads in
|
|
var result: [PendingMediaUploadKey: Float] = [:]
|
|
result.reserveCapacity(pendingMediaUploads.count)
|
|
for (messageId, progress) in pendingMediaUploads {
|
|
result[PendingMediaUploadKey(accountId: accountId, messageId: messageId)] = progress
|
|
}
|
|
return result
|
|
}
|
|
}
|
|
return combineLatest(signals)
|
|
|> map { values in
|
|
var result: [PendingMediaUploadKey: Float] = [:]
|
|
for value in values {
|
|
for (key, progress) in value {
|
|
result[key] = progress
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
}
|
|
|> distinctUntilChanged
|
|
|> deliverOnMainQueue).startStrict(next: { [weak self] pendingMediaUploadsByKey in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.pendingMediaUploadsByKey = pendingMediaUploadsByKey
|
|
strongSelf.updateBackgroundProcessingTaskStateFromPendingMediaUploads()
|
|
})
|
|
|
|
self.pendingStoryUploadsDisposable = (activeAccounts
|
|
|> deliverOnMainQueue
|
|
|> mapToSignal { _, accounts -> Signal<[PendingStoryUploadKey: PendingStoryUploadStatus], NoError> in
|
|
if accounts.isEmpty {
|
|
return .single([:])
|
|
}
|
|
let signals: [Signal<[PendingStoryUploadKey: PendingStoryUploadStatus], NoError>] = accounts.map { accountId, account in
|
|
return TelegramEngine(account: account).messages.pendingStoryUploadStatuses()
|
|
|> map { pendingStoryUploadStatuses in
|
|
var result: [PendingStoryUploadKey: PendingStoryUploadStatus] = [:]
|
|
result.reserveCapacity(pendingStoryUploadStatuses.count)
|
|
for (stableId, status) in pendingStoryUploadStatuses {
|
|
result[PendingStoryUploadKey(accountId: accountId, stableId: stableId)] = status
|
|
}
|
|
return result
|
|
}
|
|
}
|
|
return combineLatest(signals)
|
|
|> map { values in
|
|
var result: [PendingStoryUploadKey: PendingStoryUploadStatus] = [:]
|
|
for value in values {
|
|
for (key, status) in value {
|
|
result[key] = status
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
}
|
|
|> distinctUntilChanged
|
|
|> deliverOnMainQueue).startStrict(next: { [weak self] pendingStoryUploadStatusesByKey in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.pendingStoryUploadStatusesByKey = pendingStoryUploadStatusesByKey
|
|
|
|
var pendingStoryUploadsByKey: [PendingStoryUploadKey: Float] = [:]
|
|
pendingStoryUploadsByKey.reserveCapacity(pendingStoryUploadStatusesByKey.count)
|
|
for (key, status) in pendingStoryUploadStatusesByKey {
|
|
pendingStoryUploadsByKey[key] = status.progress
|
|
}
|
|
strongSelf.pendingStoryUploadsByKey = pendingStoryUploadsByKey
|
|
strongSelf.updateBackgroundProcessingTaskStateFromPendingStoryUploads()
|
|
})
|
|
}
|
|
|
|
deinit {
|
|
self.accountSettingsDisposable?.dispose()
|
|
self.inForegroundDisposable?.dispose()
|
|
self.hasActiveAudioSessionDisposable?.dispose()
|
|
self.tasksDisposable?.dispose()
|
|
self.pendingMediaUploadsDisposable?.dispose()
|
|
self.pendingStoryUploadsDisposable?.dispose()
|
|
self.managedPausedInBackgroundPlayer?.dispose()
|
|
self.keepIdleDisposable?.dispose()
|
|
if let (taskId, _, timer) = self.currentTask {
|
|
timer.invalidate()
|
|
self.endBackgroundTask(taskId)
|
|
}
|
|
}
|
|
|
|
private func updateBackgroundProcessingTaskStateFromPendingMediaUploads() {
|
|
if !self.enableBackgroundTasks {
|
|
return
|
|
}
|
|
|
|
let shouldHaveTask = !self.pendingMediaUploadsByKey.isEmpty && !self.inForeground
|
|
let hadTask = self.backgroundProcessingTaskId != nil
|
|
|
|
if shouldHaveTask {
|
|
if !hadTask {
|
|
self.startBackgroundProcessingTaskIfNeeded()
|
|
}
|
|
} else {
|
|
if let backgroundProcessingTaskId = self.backgroundProcessingTaskId {
|
|
if !self.backgroundProcessingTaskCancellationRequestedByApp {
|
|
self.backgroundProcessingTaskCancellationRequestedByApp = true
|
|
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: backgroundProcessingTaskId)
|
|
Logger.shared.log("Wakeup", "Requested BG task cancellation by app: \(backgroundProcessingTaskId)")
|
|
}
|
|
|
|
if !self.backgroundProcessingTaskLaunched {
|
|
self.backgroundProcessingTaskId = nil
|
|
self.backgroundProcessingTaskProgressByKey = [:]
|
|
self.backgroundProcessingTaskCancellationRequestedByApp = false
|
|
self.checkTasks()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func updateBackgroundProcessingTaskStateFromPendingStoryUploads() {
|
|
if !self.enableBackgroundTasks {
|
|
return
|
|
}
|
|
|
|
let shouldHaveTask = !self.pendingStoryUploadStatusesByKey.isEmpty && !self.inForeground
|
|
let hadTask = self.backgroundStoryProcessingTaskId != nil
|
|
|
|
if shouldHaveTask {
|
|
if !hadTask {
|
|
self.startBackgroundStoryProcessingTaskIfNeeded()
|
|
}
|
|
} else {
|
|
if let backgroundStoryProcessingTaskId = self.backgroundStoryProcessingTaskId {
|
|
if !self.backgroundStoryProcessingTaskCancellationRequestedByApp {
|
|
self.backgroundStoryProcessingTaskCancellationRequestedByApp = true
|
|
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: backgroundStoryProcessingTaskId)
|
|
Logger.shared.log("Wakeup", "Requested story BG task cancellation by app: \(backgroundStoryProcessingTaskId)")
|
|
}
|
|
|
|
if !self.backgroundStoryProcessingTaskLaunched {
|
|
self.backgroundStoryProcessingTaskId = nil
|
|
self.backgroundStoryProcessingTaskProgressByKey = [:]
|
|
self.backgroundStoryProcessingTaskCancellationRequestedByApp = false
|
|
self.checkTasks()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func cancelUploadingMessagesForCurrentTask() {
|
|
let keys = Array(self.pendingMediaUploadsByKey.keys)
|
|
if keys.isEmpty {
|
|
Logger.shared.log("Wakeup", "BG task external cancel: no pending uploads to delete")
|
|
return
|
|
}
|
|
|
|
var messageIdsByAccount: [AccountRecordId: [MessageId]] = [:]
|
|
for key in keys {
|
|
if messageIdsByAccount[key.accountId] == nil {
|
|
messageIdsByAccount[key.accountId] = []
|
|
}
|
|
messageIdsByAccount[key.accountId]?.append(key.messageId)
|
|
}
|
|
|
|
for key in keys {
|
|
self.pendingMediaUploadsByKey.removeValue(forKey: key)
|
|
}
|
|
|
|
Logger.shared.log("Wakeup", "BG task external cancel: deleting \(keys.count) uploading messages across \(messageIdsByAccount.count) accounts")
|
|
|
|
for (accountId, messageIds) in messageIdsByAccount {
|
|
guard let account = self.accountsAndTasks.first(where: { $0.0.id == accountId })?.0 else {
|
|
Logger.shared.log("Wakeup", "BG task external cancel: missing account \(accountId.int64), skip \(messageIds.count) messages")
|
|
continue
|
|
}
|
|
Logger.shared.log("Wakeup", "BG task external cancel: deleting \(messageIds.count) messages in account \(accountId.int64)")
|
|
let _ = TelegramEngine(account: account).messages.deleteMessagesInteractively(messageIds: messageIds, type: .forLocalPeer).startStandalone()
|
|
}
|
|
}
|
|
|
|
private func cancelUploadingStoriesForCurrentTask() {
|
|
let keys = Array(self.pendingStoryUploadsByKey.keys)
|
|
if keys.isEmpty {
|
|
Logger.shared.log("Wakeup", "Story BG task external cancel: no pending uploads to cancel")
|
|
return
|
|
}
|
|
|
|
var stableIdsByAccount: [AccountRecordId: [Int32]] = [:]
|
|
for key in keys {
|
|
if stableIdsByAccount[key.accountId] == nil {
|
|
stableIdsByAccount[key.accountId] = []
|
|
}
|
|
stableIdsByAccount[key.accountId]?.append(key.stableId)
|
|
}
|
|
|
|
for key in keys {
|
|
self.pendingStoryUploadsByKey.removeValue(forKey: key)
|
|
self.pendingStoryUploadStatusesByKey.removeValue(forKey: key)
|
|
}
|
|
|
|
Logger.shared.log("Wakeup", "Story BG task external cancel: cancelling \(keys.count) uploading stories across \(stableIdsByAccount.count) accounts")
|
|
|
|
for (accountId, stableIds) in stableIdsByAccount {
|
|
guard let account = self.accountsAndTasks.first(where: { $0.0.id == accountId })?.0 else {
|
|
Logger.shared.log("Wakeup", "Story BG task external cancel: missing account \(accountId.int64), skip \(stableIds.count) stories")
|
|
continue
|
|
}
|
|
Logger.shared.log("Wakeup", "Story BG task external cancel: cancelling \(stableIds.count) stories in account \(accountId.int64)")
|
|
let engineMessages = TelegramEngine(account: account).messages
|
|
for stableId in stableIds {
|
|
engineMessages.cancelStoryUpload(stableId: stableId)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func startBackgroundProcessingTaskIfNeeded() {
|
|
guard #available(iOS 26.0, *) else {
|
|
return
|
|
}
|
|
guard !self.inForeground else {
|
|
return
|
|
}
|
|
guard self.backgroundProcessingTaskId == nil else {
|
|
return
|
|
}
|
|
guard let presentationData = self.presentationData() else {
|
|
return
|
|
}
|
|
|
|
let baseAppBundleId = Bundle.main.bundleIdentifier!
|
|
let uploadTaskId = "\(baseAppBundleId).upload.message\(self.nextBackgroundProcessingTaskId)"
|
|
self.nextBackgroundProcessingTaskId += 1
|
|
self.backgroundProcessingTaskProgressByKey = [:]
|
|
self.backgroundProcessingTaskLaunched = false
|
|
self.backgroundProcessingTaskCancellationRequestedByApp = false
|
|
|
|
BGTaskScheduler.shared.register(forTaskWithIdentifier: uploadTaskId, using: nil, launchHandler: { [weak self] task in
|
|
guard let task = task as? BGContinuedProcessingTask else {
|
|
return
|
|
}
|
|
guard let self else {
|
|
task.updateTitle(task.title, subtitle: presentationData.strings.BackgroundTasks_MediaFinished)
|
|
task.setTaskCompleted(success: true)
|
|
return
|
|
}
|
|
|
|
Task { @MainActor [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
if self.backgroundProcessingTaskId == task.identifier {
|
|
self.backgroundProcessingTaskLaunched = true
|
|
}
|
|
}
|
|
|
|
var wasExpired = false
|
|
|
|
task.expirationHandler = { [weak self] in
|
|
wasExpired = true
|
|
|
|
Queue.mainQueue().async {
|
|
guard let self else {
|
|
return
|
|
}
|
|
if self.backgroundProcessingTaskId == task.identifier {
|
|
let cancelledByApp = self.backgroundProcessingTaskCancellationRequestedByApp
|
|
self.backgroundProcessingTaskCancellationRequestedByApp = false
|
|
if cancelledByApp {
|
|
Logger.shared.log("Wakeup", "BG task expired after app cancellation: \(task.identifier)")
|
|
} else {
|
|
Logger.shared.log("Wakeup", "BG task expired externally, will delete uploading messages: \(task.identifier)")
|
|
self.cancelUploadingMessagesForCurrentTask()
|
|
}
|
|
self.backgroundProcessingTaskId = nil
|
|
self.backgroundProcessingTaskProgressByKey = [:]
|
|
self.backgroundProcessingTaskLaunched = false
|
|
self.checkTasks()
|
|
self.updateBackgroundProcessingTaskStateFromPendingMediaUploads()
|
|
} else if !self.backgroundProcessingTaskCancellationRequestedByApp {
|
|
Logger.shared.log("Wakeup", "Non-current BG task expired externally, will delete uploading messages: \(task.identifier)")
|
|
self.cancelUploadingMessagesForCurrentTask()
|
|
self.checkTasks()
|
|
self.updateBackgroundProcessingTaskStateFromPendingMediaUploads()
|
|
}
|
|
}
|
|
}
|
|
|
|
Task { @MainActor [weak self] in
|
|
guard let self else {
|
|
task.updateTitle(task.title, subtitle: presentationData.strings.BackgroundTasks_MediaFinished)
|
|
task.setTaskCompleted(success: true)
|
|
return
|
|
}
|
|
|
|
var foregroundCancellationRequested = false
|
|
|
|
while true {
|
|
if wasExpired {
|
|
break
|
|
}
|
|
|
|
if self.backgroundProcessingTaskId != task.identifier || self.pendingMediaUploadsByKey.isEmpty {
|
|
self.backgroundProcessingTaskProgressByKey = [:]
|
|
task.updateTitle(task.title, subtitle: presentationData.strings.BackgroundTasks_MediaFinished)
|
|
task.setTaskCompleted(success: true)
|
|
if self.backgroundProcessingTaskId == task.identifier {
|
|
self.backgroundProcessingTaskId = nil
|
|
self.backgroundProcessingTaskLaunched = false
|
|
self.backgroundProcessingTaskCancellationRequestedByApp = false
|
|
self.checkTasks()
|
|
self.updateBackgroundProcessingTaskStateFromPendingMediaUploads()
|
|
}
|
|
return
|
|
}
|
|
|
|
if self.inForeground {
|
|
if !foregroundCancellationRequested {
|
|
foregroundCancellationRequested = true
|
|
if !self.backgroundProcessingTaskCancellationRequestedByApp {
|
|
self.backgroundProcessingTaskCancellationRequestedByApp = true
|
|
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: task.identifier)
|
|
Logger.shared.log("Wakeup", "Requested BG task cancellation due to foreground: \(task.identifier)")
|
|
}
|
|
self.backgroundProcessingTaskProgressByKey = [:]
|
|
}
|
|
try await Task.sleep(for: .seconds(1.0))
|
|
continue
|
|
} else {
|
|
foregroundCancellationRequested = false
|
|
}
|
|
|
|
if self.backgroundProcessingTaskId != task.identifier {
|
|
return
|
|
}
|
|
|
|
var currentKeys = Set<PendingMediaUploadKey>()
|
|
for (key, progress) in self.pendingMediaUploadsByKey {
|
|
currentKeys.insert(key)
|
|
let clampedProgress = min(1.0, max(0.0, progress))
|
|
if let currentProgress = self.backgroundProcessingTaskProgressByKey[key] {
|
|
self.backgroundProcessingTaskProgressByKey[key] = max(currentProgress, clampedProgress)
|
|
} else {
|
|
self.backgroundProcessingTaskProgressByKey[key] = clampedProgress
|
|
}
|
|
}
|
|
for key in self.backgroundProcessingTaskProgressByKey.keys where !currentKeys.contains(key) {
|
|
self.backgroundProcessingTaskProgressByKey[key] = 1.0
|
|
}
|
|
|
|
let progressPrecision: Int64 = 1000
|
|
let totalItemCount = max(1, self.backgroundProcessingTaskProgressByKey.count)
|
|
let totalUnitCount = Int64(totalItemCount) * progressPrecision
|
|
|
|
var completedUnitCount: Int64 = 0
|
|
for progress in self.backgroundProcessingTaskProgressByKey.values {
|
|
completedUnitCount += Int64((progress * Float(progressPrecision)).rounded(.down))
|
|
}
|
|
completedUnitCount = min(totalUnitCount, max(0, completedUnitCount))
|
|
|
|
task.progress.totalUnitCount = totalUnitCount
|
|
task.progress.completedUnitCount = completedUnitCount
|
|
|
|
let title: String = presentationData.strings.BackgroundTasks_UploadingMedia(Int32(self.pendingMediaUploadsByKey.count))
|
|
if task.title != title {
|
|
task.updateTitle(title, subtitle: presentationData.strings.BackgroundTasks_MediaSubtitle)
|
|
}
|
|
|
|
try await Task.sleep(for: .seconds(1.0))
|
|
}
|
|
}
|
|
})
|
|
|
|
let title: String = presentationData.strings.BackgroundTasks_UploadingMedia(Int32(self.pendingMediaUploadsByKey.count))
|
|
|
|
let request = BGContinuedProcessingTaskRequest(
|
|
identifier: uploadTaskId,
|
|
title: title,
|
|
subtitle: presentationData.strings.BackgroundTasks_MediaSubtitle
|
|
)
|
|
request.strategy = .fail
|
|
|
|
do {
|
|
try BGTaskScheduler.shared.submit(request)
|
|
self.backgroundProcessingTaskId = uploadTaskId
|
|
self.backgroundProcessingTaskLaunched = false
|
|
self.checkTasks()
|
|
} catch let e {
|
|
Logger.shared.log("Wakeup", "BGTaskScheduler submit error: \(e)")
|
|
}
|
|
}
|
|
|
|
private func startBackgroundStoryProcessingTaskIfNeeded() {
|
|
guard #available(iOS 26.0, *) else {
|
|
return
|
|
}
|
|
guard !self.inForeground else {
|
|
return
|
|
}
|
|
guard self.backgroundStoryProcessingTaskId == nil else {
|
|
return
|
|
}
|
|
guard let presentationData = self.presentationData() else {
|
|
return
|
|
}
|
|
|
|
let baseAppBundleId = Bundle.main.bundleIdentifier!
|
|
let uploadTaskId = "\(baseAppBundleId).upload.story\(self.nextBackgroundStoryProcessingTaskId)"
|
|
self.nextBackgroundStoryProcessingTaskId += 1
|
|
self.backgroundStoryProcessingTaskProgressByKey = [:]
|
|
self.backgroundStoryProcessingTaskLaunched = false
|
|
self.backgroundStoryProcessingTaskCancellationRequestedByApp = false
|
|
|
|
BGTaskScheduler.shared.register(forTaskWithIdentifier: uploadTaskId, using: nil, launchHandler: { [weak self] task in
|
|
guard let task = task as? BGContinuedProcessingTask else {
|
|
return
|
|
}
|
|
guard let self else {
|
|
task.updateTitle(task.title, subtitle: presentationData.strings.BackgroundTasks_StoryFinished)
|
|
task.setTaskCompleted(success: true)
|
|
return
|
|
}
|
|
|
|
Task { @MainActor [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
if self.backgroundStoryProcessingTaskId == task.identifier {
|
|
self.backgroundStoryProcessingTaskLaunched = true
|
|
}
|
|
}
|
|
|
|
var wasExpired = false
|
|
|
|
task.expirationHandler = { [weak self] in
|
|
wasExpired = true
|
|
|
|
Queue.mainQueue().async {
|
|
guard let self else {
|
|
return
|
|
}
|
|
if self.backgroundStoryProcessingTaskId == task.identifier {
|
|
let cancelledByApp = self.backgroundStoryProcessingTaskCancellationRequestedByApp
|
|
self.backgroundStoryProcessingTaskCancellationRequestedByApp = false
|
|
if cancelledByApp {
|
|
Logger.shared.log("Wakeup", "Story BG task expired after app cancellation: \(task.identifier)")
|
|
} else {
|
|
Logger.shared.log("Wakeup", "Story BG task expired externally, will cancel uploading stories: \(task.identifier)")
|
|
self.cancelUploadingStoriesForCurrentTask()
|
|
}
|
|
self.backgroundStoryProcessingTaskId = nil
|
|
self.backgroundStoryProcessingTaskProgressByKey = [:]
|
|
self.backgroundStoryProcessingTaskLaunched = false
|
|
self.checkTasks()
|
|
self.updateBackgroundProcessingTaskStateFromPendingStoryUploads()
|
|
} else if !self.backgroundStoryProcessingTaskCancellationRequestedByApp {
|
|
Logger.shared.log("Wakeup", "Non-current story BG task expired externally, will cancel uploading stories: \(task.identifier)")
|
|
self.cancelUploadingStoriesForCurrentTask()
|
|
self.checkTasks()
|
|
self.updateBackgroundProcessingTaskStateFromPendingStoryUploads()
|
|
}
|
|
}
|
|
}
|
|
|
|
Task { @MainActor [weak self] in
|
|
guard let self else {
|
|
task.updateTitle(task.title, subtitle: presentationData.strings.BackgroundTasks_StoryFinished)
|
|
task.setTaskCompleted(success: true)
|
|
return
|
|
}
|
|
|
|
var foregroundCancellationRequested = false
|
|
var currentDisplayedTitle: String?
|
|
var currentDisplayedSubtitle: String?
|
|
|
|
while true {
|
|
if wasExpired {
|
|
break
|
|
}
|
|
|
|
if self.backgroundStoryProcessingTaskId != task.identifier || self.pendingStoryUploadStatusesByKey.isEmpty {
|
|
self.backgroundStoryProcessingTaskProgressByKey = [:]
|
|
task.updateTitle(task.title, subtitle: presentationData.strings.BackgroundTasks_StoryFinished)
|
|
task.setTaskCompleted(success: true)
|
|
if self.backgroundStoryProcessingTaskId == task.identifier {
|
|
self.backgroundStoryProcessingTaskId = nil
|
|
self.backgroundStoryProcessingTaskLaunched = false
|
|
self.backgroundStoryProcessingTaskCancellationRequestedByApp = false
|
|
self.checkTasks()
|
|
self.updateBackgroundProcessingTaskStateFromPendingStoryUploads()
|
|
}
|
|
return
|
|
}
|
|
|
|
if self.inForeground {
|
|
if !foregroundCancellationRequested {
|
|
foregroundCancellationRequested = true
|
|
if !self.backgroundStoryProcessingTaskCancellationRequestedByApp {
|
|
self.backgroundStoryProcessingTaskCancellationRequestedByApp = true
|
|
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: task.identifier)
|
|
Logger.shared.log("Wakeup", "Requested story BG task cancellation due to foreground: \(task.identifier)")
|
|
}
|
|
self.backgroundStoryProcessingTaskProgressByKey = [:]
|
|
}
|
|
try await Task.sleep(for: .seconds(1.0))
|
|
continue
|
|
} else {
|
|
foregroundCancellationRequested = false
|
|
}
|
|
|
|
if self.backgroundStoryProcessingTaskId != task.identifier {
|
|
return
|
|
}
|
|
|
|
var currentKeys = Set<PendingStoryUploadKey>()
|
|
for (key, status) in self.pendingStoryUploadStatusesByKey {
|
|
currentKeys.insert(key)
|
|
let clampedProgress = min(1.0, max(0.0, status.progress))
|
|
if let currentProgress = self.backgroundStoryProcessingTaskProgressByKey[key] {
|
|
self.backgroundStoryProcessingTaskProgressByKey[key] = max(currentProgress, clampedProgress)
|
|
} else {
|
|
self.backgroundStoryProcessingTaskProgressByKey[key] = clampedProgress
|
|
}
|
|
}
|
|
for key in self.backgroundStoryProcessingTaskProgressByKey.keys where !currentKeys.contains(key) {
|
|
self.backgroundStoryProcessingTaskProgressByKey[key] = 1.0
|
|
}
|
|
|
|
let progressPrecision: Int64 = 1000
|
|
let totalItemCount = max(1, self.backgroundStoryProcessingTaskProgressByKey.count)
|
|
let totalUnitCount = Int64(totalItemCount) * progressPrecision
|
|
|
|
var completedUnitCount: Int64 = 0
|
|
for progress in self.backgroundStoryProcessingTaskProgressByKey.values {
|
|
completedUnitCount += Int64((progress * Float(progressPrecision)).rounded(.down))
|
|
}
|
|
completedUnitCount = min(totalUnitCount, max(0, completedUnitCount))
|
|
|
|
task.progress.totalUnitCount = totalUnitCount
|
|
task.progress.completedUnitCount = completedUnitCount
|
|
|
|
let title: String = presentationData.strings.BackgroundTasks_UploadingStories(Int32(self.pendingStoryUploadsByKey.count))
|
|
let subtitle: String
|
|
if self.pendingStoryUploadStatusesByKey.values.contains(where: { $0.phase == .processing }) {
|
|
subtitle = presentationData.strings.BackgroundTasks_StoryOpenAppToContinue
|
|
} else {
|
|
subtitle = presentationData.strings.BackgroundTasks_StorySubtitle
|
|
}
|
|
if currentDisplayedTitle != title || currentDisplayedSubtitle != subtitle {
|
|
task.updateTitle(title, subtitle: subtitle)
|
|
currentDisplayedTitle = title
|
|
currentDisplayedSubtitle = subtitle
|
|
}
|
|
|
|
try await Task.sleep(for: .seconds(1.0))
|
|
}
|
|
}
|
|
})
|
|
|
|
let title: String = presentationData.strings.BackgroundTasks_UploadingStories(Int32(self.pendingStoryUploadsByKey.count))
|
|
let subtitle: String
|
|
if self.pendingStoryUploadStatusesByKey.values.contains(where: { $0.phase == .processing }) {
|
|
subtitle = presentationData.strings.BackgroundTasks_StoryOpenAppToContinue
|
|
} else {
|
|
subtitle = presentationData.strings.BackgroundTasks_StorySubtitle
|
|
}
|
|
|
|
let request = BGContinuedProcessingTaskRequest(
|
|
identifier: uploadTaskId,
|
|
title: title,
|
|
subtitle: subtitle
|
|
)
|
|
request.strategy = .fail
|
|
/*if BGTaskScheduler.supportedResources.contains(.gpu) {
|
|
request.requiredResources = .gpu
|
|
}*/
|
|
|
|
do {
|
|
try BGTaskScheduler.shared.submit(request)
|
|
self.backgroundStoryProcessingTaskId = uploadTaskId
|
|
self.backgroundStoryProcessingTaskLaunched = false
|
|
self.checkTasks()
|
|
} catch let e {
|
|
Logger.shared.log("Wakeup", "Story BGTaskScheduler submit error: \(e)")
|
|
}
|
|
}
|
|
|
|
func allowBackgroundTimeExtension(timeout: Double, extendNow: Bool = false) {
|
|
let shouldCheckTasks = self.allowBackgroundTimeExtensionDeadline == nil
|
|
self.allowBackgroundTimeExtensionDeadline = CFAbsoluteTimeGetCurrent() + timeout
|
|
|
|
self.allowBackgroundTimeExtensionDeadlineTimer?.invalidate()
|
|
self.allowBackgroundTimeExtensionDeadlineTimer = SwiftSignalKit.Timer(timeout: timeout, repeat: false, completion: { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.allowBackgroundTimeExtensionDeadlineTimer?.invalidate()
|
|
strongSelf.allowBackgroundTimeExtensionDeadlineTimer = nil
|
|
strongSelf.checkTasks()
|
|
}, queue: .mainQueue())
|
|
self.allowBackgroundTimeExtensionDeadlineTimer?.start()
|
|
|
|
if extendNow {
|
|
if self.activeExplicitExtensionTimer == nil {
|
|
let activeExplicitExtensionTimer = SwiftSignalKit.Timer(timeout: 20.0, repeat: false, completion: { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.activeExplicitExtensionTimer?.invalidate()
|
|
strongSelf.activeExplicitExtensionTimer = nil
|
|
if let activeExplicitExtensionTask = strongSelf.activeExplicitExtensionTask {
|
|
strongSelf.activeExplicitExtensionTask = nil
|
|
strongSelf.endBackgroundTask(activeExplicitExtensionTask)
|
|
}
|
|
strongSelf.checkTasks()
|
|
}, queue: .mainQueue())
|
|
self.activeExplicitExtensionTimer = activeExplicitExtensionTimer
|
|
activeExplicitExtensionTimer.start()
|
|
|
|
self.activeExplicitExtensionTask = self.beginBackgroundTask("explicit-extension") { [weak self, weak activeExplicitExtensionTimer] in
|
|
guard let self, let activeExplicitExtensionTimer else {
|
|
return
|
|
}
|
|
if self.activeExplicitExtensionTimer === activeExplicitExtensionTimer {
|
|
self.activeExplicitExtensionTimer?.invalidate()
|
|
self.activeExplicitExtensionTimer = nil
|
|
if let activeExplicitExtensionTask = self.activeExplicitExtensionTask {
|
|
self.activeExplicitExtensionTask = nil
|
|
self.endBackgroundTask(activeExplicitExtensionTask)
|
|
}
|
|
self.checkTasks()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if shouldCheckTasks || extendNow {
|
|
self.checkTasks()
|
|
}
|
|
}
|
|
|
|
func replaceCurrentExtensionWithExternalTime(completion: @escaping () -> Void, timeout: Double) {
|
|
if let (currentCompletion, timer) = self.currentExternalCompletion {
|
|
currentCompletion()
|
|
timer.invalidate()
|
|
self.currentExternalCompletion = nil
|
|
}
|
|
let timer = SwiftSignalKit.Timer(timeout: timeout - 5.0, repeat: false, completion: { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.currentExternalCompletionValidationTimer?.invalidate()
|
|
strongSelf.currentExternalCompletionValidationTimer = nil
|
|
if let (completion, timer) = strongSelf.currentExternalCompletion {
|
|
strongSelf.currentExternalCompletion = nil
|
|
timer.invalidate()
|
|
completion()
|
|
}
|
|
strongSelf.checkTasks()
|
|
}, queue: Queue.mainQueue())
|
|
self.currentExternalCompletion = (completion, timer)
|
|
timer.start()
|
|
|
|
self.currentExternalCompletionValidationTimer?.invalidate()
|
|
let validationTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: false, completion: { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.currentExternalCompletionValidationTimer?.invalidate()
|
|
strongSelf.currentExternalCompletionValidationTimer = nil
|
|
strongSelf.checkTasks()
|
|
}, queue: Queue.mainQueue())
|
|
self.currentExternalCompletionValidationTimer = validationTimer
|
|
validationTimer.start()
|
|
self.checkTasks()
|
|
}
|
|
|
|
func checkTasks() {
|
|
var hasTasksForBackgroundExtension = false
|
|
|
|
var hasActiveCalls = false
|
|
var pendingMessageCount = 0
|
|
for (_, _, tasks) in self.accountsAndTasks {
|
|
if tasks.activeCalls {
|
|
hasActiveCalls = true
|
|
}
|
|
pendingMessageCount += tasks.importantTasks.pendingMessageCount
|
|
}
|
|
|
|
var endTaskAfterTransactionsComplete: UIBackgroundTaskIdentifier?
|
|
|
|
if self.inForeground || self.hasActiveAudioSession || hasActiveCalls {
|
|
if let (completion, timer) = self.currentExternalCompletion {
|
|
self.currentExternalCompletion = nil
|
|
completion()
|
|
timer.invalidate()
|
|
}
|
|
|
|
if let (taskId, _, timer) = self.currentTask {
|
|
self.currentTask = nil
|
|
timer.invalidate()
|
|
self.endBackgroundTask(taskId)
|
|
self.isInBackgroundExtension = false
|
|
}
|
|
} else {
|
|
for (_, _, tasks) in self.accountsAndTasks {
|
|
if !tasks.isEmpty {
|
|
hasTasksForBackgroundExtension = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !hasTasksForBackgroundExtension && self.currentExternalCompletionValidationTimer == nil {
|
|
if let (completion, timer) = self.currentExternalCompletion {
|
|
self.currentExternalCompletion = nil
|
|
completion()
|
|
timer.invalidate()
|
|
}
|
|
}
|
|
|
|
if self.activeExplicitExtensionTimer != nil {
|
|
hasTasksForBackgroundExtension = true
|
|
}
|
|
|
|
let canBeginBackgroundExtensionTasks = self.allowBackgroundTimeExtensionDeadline.flatMap({ CFAbsoluteTimeGetCurrent() < $0 }) ?? false
|
|
if hasTasksForBackgroundExtension {
|
|
if canBeginBackgroundExtensionTasks {
|
|
var endTaskId: UIBackgroundTaskIdentifier?
|
|
|
|
let currentTime = CFAbsoluteTimeGetCurrent()
|
|
if let (taskId, startTime, timer) = self.currentTask {
|
|
if startTime < currentTime + 1.0 {
|
|
self.currentTask = nil
|
|
timer.invalidate()
|
|
endTaskId = taskId
|
|
}
|
|
}
|
|
|
|
if self.currentTask == nil {
|
|
var actualTaskId: UIBackgroundTaskIdentifier?
|
|
let handleExpiration: () -> Void = { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
|
|
if let actualTaskId {
|
|
strongSelf.endBackgroundTask(actualTaskId)
|
|
|
|
if let (taskId, _, timer) = strongSelf.currentTask, taskId == actualTaskId {
|
|
timer.invalidate()
|
|
strongSelf.currentTask = nil
|
|
}
|
|
}
|
|
|
|
strongSelf.isInBackgroundExtension = false
|
|
strongSelf.checkTasks()
|
|
}
|
|
if let taskId = self.beginBackgroundTask("background-wakeup", {
|
|
handleExpiration()
|
|
}) {
|
|
actualTaskId = taskId
|
|
let timer = SwiftSignalKit.Timer(timeout: min(30.0, max(0.0, self.backgroundTimeRemaining() - 5.0)), repeat: false, completion: {
|
|
handleExpiration()
|
|
}, queue: Queue.mainQueue())
|
|
self.currentTask = (taskId, currentTime, timer)
|
|
timer.start()
|
|
|
|
endTaskId.flatMap(self.endBackgroundTask)
|
|
|
|
self.isInBackgroundExtension = true
|
|
}
|
|
}
|
|
}
|
|
} else if let (taskId, _, timer) = self.currentTask {
|
|
self.currentTask = nil
|
|
|
|
timer.invalidate()
|
|
|
|
endTaskAfterTransactionsComplete = taskId
|
|
|
|
self.isInBackgroundExtension = false
|
|
}
|
|
}
|
|
|
|
if pendingMessageCount != 0 && !self.inForeground {
|
|
if self.keepIdleDisposable == nil {
|
|
self.keepIdleDisposable = self.acquireIdleExtension()
|
|
}
|
|
} else {
|
|
if let keepIdleDisposable = self.keepIdleDisposable {
|
|
self.keepIdleDisposable = nil
|
|
keepIdleDisposable.dispose()
|
|
}
|
|
}
|
|
|
|
self.updateAccounts(hasTasks: hasTasksForBackgroundExtension, endTaskAfterTransactionsComplete: endTaskAfterTransactionsComplete)
|
|
|
|
/*if !self.inForeground && pendingMessageCount != 0 && !self.hasActiveAudioSession {
|
|
if self.silenceAudioRenderer == nil {
|
|
let audioSession = AVAudioSession()
|
|
let _ = try? audioSession.setCategory(.ambient)
|
|
let _ = try? audioSession.setMode(.default)
|
|
let silenceAudioRenderer = MediaPlayerAudioRenderer(
|
|
audioSession: .custom({ control in
|
|
let _ = try? audioSession.setActive(true)
|
|
control.activate()
|
|
|
|
return EmptyDisposable
|
|
}),
|
|
forAudioVideoMessage: false,
|
|
playAndRecord: false,
|
|
useVoiceProcessingMode: false,
|
|
soundMuted: false,
|
|
ambient: true,
|
|
mixWithOthers: true,
|
|
forceAudioToSpeaker: false,
|
|
baseRate: 1.0,
|
|
audioLevelPipe: ValuePipe(),
|
|
updatedRate: {},
|
|
audioPaused: {}
|
|
)
|
|
self.silenceAudioRenderer = silenceAudioRenderer
|
|
silenceAudioRenderer.start()
|
|
}
|
|
} else if let silenceAudioRenderer = self.silenceAudioRenderer {
|
|
self.silenceAudioRenderer = nil
|
|
silenceAudioRenderer.stop()
|
|
}*/
|
|
}
|
|
|
|
private func updateAccounts(hasTasks: Bool, endTaskAfterTransactionsComplete: UIBackgroundTaskIdentifier?) {
|
|
if self.inForeground || self.hasActiveAudioSession || self.isInBackgroundExtension || self.backgroundProcessingTaskId != nil || self.backgroundStoryProcessingTaskId != nil || (hasTasks && self.currentExternalCompletion != nil) || self.activeExplicitExtensionTimer != nil || self.silenceAudioRenderer != nil {
|
|
Logger.shared.log("Wakeup", "enableBeginTransactions: true (active)")
|
|
|
|
for (account, primary, tasks) in self.accountsAndTasks {
|
|
account.postbox.setCanBeginTransactions(true)
|
|
|
|
if (self.inForeground && primary) || !tasks.isEmpty || (self.activeExplicitExtensionTimer != nil && primary) {
|
|
account.shouldBeServiceTaskMaster.set(.single(.always))
|
|
} else {
|
|
account.shouldBeServiceTaskMaster.set(.single(.never))
|
|
}
|
|
account.shouldExplicitelyKeepWorkerConnections.set(.single(tasks.backgroundAudio || tasks.importantTasks.pendingStoryCount != 0 || tasks.importantTasks.pendingMessageCount != 0))
|
|
account.shouldKeepOnlinePresence.set(.single(primary && self.inForeground))
|
|
account.shouldKeepBackgroundDownloadConnections.set(.single(tasks.backgroundDownloads))
|
|
}
|
|
|
|
if let endTaskAfterTransactionsComplete {
|
|
self.endBackgroundTask(endTaskAfterTransactionsComplete)
|
|
}
|
|
} else {
|
|
var enableBeginTransactions = false
|
|
if self.allowBackgroundTimeExtensionDeadlineTimer != nil {
|
|
enableBeginTransactions = true
|
|
}
|
|
Logger.shared.log("Wakeup", "enableBeginTransactions: \(enableBeginTransactions)")
|
|
|
|
final class CompletionObservationState {
|
|
var isCompleted: Bool = false
|
|
var remainingAccounts: [AccountRecordId]
|
|
|
|
init(remainingAccounts: [AccountRecordId]) {
|
|
self.remainingAccounts = remainingAccounts
|
|
}
|
|
}
|
|
let completionState = Atomic<CompletionObservationState>(value: CompletionObservationState(remainingAccounts: self.accountsAndTasks.map(\.0.id)))
|
|
let checkCompletionState: (AccountRecordId?) -> Void = { id in
|
|
Queue.mainQueue().async {
|
|
var shouldComplete = false
|
|
completionState.with { state in
|
|
if let id {
|
|
state.remainingAccounts.removeAll(where: { $0 == id })
|
|
}
|
|
if state.remainingAccounts.isEmpty && !state.isCompleted {
|
|
state.isCompleted = true
|
|
shouldComplete = true
|
|
}
|
|
}
|
|
if shouldComplete, let endTaskAfterTransactionsComplete {
|
|
self.endBackgroundTask(endTaskAfterTransactionsComplete)
|
|
}
|
|
}
|
|
}
|
|
|
|
for (account, _, _) in self.accountsAndTasks {
|
|
let accountId = account.id
|
|
account.postbox.setCanBeginTransactions(enableBeginTransactions, afterTransactionIfRunning: {
|
|
checkCompletionState(accountId)
|
|
})
|
|
account.shouldBeServiceTaskMaster.set(.single(.never))
|
|
account.shouldKeepOnlinePresence.set(.single(false))
|
|
account.shouldKeepBackgroundDownloadConnections.set(.single(false))
|
|
}
|
|
|
|
checkCompletionState(nil)
|
|
}
|
|
}
|
|
}
|