mirror of
https://github.com/ProtonMail/ios-mail.git
synced 2026-05-15 09:50:39 +00:00
165 lines
6.4 KiB
Swift
165 lines
6.4 KiB
Swift
// Copyright (c) 2025 Proton Technologies AG
|
|
//
|
|
// This file is part of Proton Mail.
|
|
//
|
|
// Proton Mail is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// Proton Mail is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with Proton Mail. If not, see https://www.gnu.org/licenses/.
|
|
|
|
import Combine
|
|
import BackgroundTasks
|
|
import InboxCore
|
|
import proton_app_uniffi
|
|
|
|
class RecurringBackgroundTaskScheduler: @unchecked Sendable {
|
|
typealias BackgroundTaskExecutorProvider = () -> BackgroundTaskExecutor
|
|
private static let identifier = "\(Bundle.defaultIdentifier).execute_pending_actions"
|
|
private let sessionStatePublisher: AnyPublisher<SessionState, Never>
|
|
private let sessionState: () -> SessionState
|
|
private let timerFactory: TimerPublisherFactory
|
|
private let backgroundTaskRegistration: BackgroundTaskRegistration
|
|
private let backgroundTaskScheduler: BackgroundTaskScheduler
|
|
private let backgroundTaskExecutorProvider: BackgroundTaskExecutorProvider
|
|
private var callback: BackgroundExecutionCallbackWrapper!
|
|
private let backgroundTaskExpired = PassthroughSubject<Void, Never>()
|
|
private var sessionSetUpCheckCancellable: AnyCancellable?
|
|
|
|
convenience init(backgroundTaskExecutorProvider: @escaping BackgroundTaskExecutorProvider) {
|
|
self.init(
|
|
backgroundTaskRegistration: .init(registerWithIdentifier: BGTaskScheduler.shared.register),
|
|
backgroundTaskScheduler: BGTaskScheduler.shared,
|
|
backgroundTaskExecutorProvider: backgroundTaskExecutorProvider
|
|
)
|
|
}
|
|
|
|
init(
|
|
sessionStatePublisher: AnyPublisher<SessionState, Never> = AppContext.shared.$sessionState.eraseToAnyPublisher(),
|
|
sessionState: @escaping () -> SessionState = { AppContext.shared.sessionState },
|
|
timerFactory: @escaping TimerPublisherFactory = TimerFactory.make,
|
|
backgroundTaskRegistration: BackgroundTaskRegistration,
|
|
backgroundTaskScheduler: BackgroundTaskScheduler,
|
|
backgroundTaskExecutorProvider: @escaping BackgroundTaskExecutorProvider
|
|
) {
|
|
self.sessionStatePublisher = sessionStatePublisher
|
|
self.sessionState = sessionState
|
|
self.timerFactory = timerFactory
|
|
self.backgroundTaskRegistration = backgroundTaskRegistration
|
|
self.backgroundTaskScheduler = backgroundTaskScheduler
|
|
self.backgroundTaskExecutorProvider = backgroundTaskExecutorProvider
|
|
}
|
|
|
|
func register() {
|
|
let isTaskDefinedInInfoPlist = backgroundTaskRegistration.registerWithIdentifier(
|
|
Self.identifier,
|
|
nil
|
|
) { task in
|
|
Task { [weak self] in
|
|
log("Background task execution started")
|
|
await self?.execute(task: task)
|
|
}
|
|
}
|
|
if !isTaskDefinedInInfoPlist {
|
|
fatalError("Missing background task identifier: <\(Self.identifier)> in the Info.plist file.")
|
|
}
|
|
log("Background task registered")
|
|
}
|
|
|
|
func submit() async {
|
|
let allTaskRequests = await backgroundTaskScheduler.pendingTaskRequests()
|
|
let isTaskSchedulled =
|
|
allTaskRequests
|
|
.contains(where: { request in request.identifier == Self.identifier })
|
|
guard !isTaskSchedulled else {
|
|
return
|
|
}
|
|
|
|
do {
|
|
try backgroundTaskScheduler.submit(taskRequest)
|
|
log("Background task submitted")
|
|
} catch {
|
|
log("Background task failed to submit, because of error: \(error.localizedDescription)")
|
|
}
|
|
}
|
|
|
|
func cancel() {
|
|
backgroundTaskScheduler.cancel(taskRequestWithIdentifier: Self.identifier)
|
|
}
|
|
|
|
// MARK: - Private
|
|
|
|
private func execute(task: BackgroundTask) async {
|
|
if sessionState().isAuthorized {
|
|
await submit()
|
|
}
|
|
|
|
callback = .init { [weak self] result in
|
|
self?.backgroundExecutionHasCompleted(completionStatus: result.status, task: task)
|
|
}
|
|
|
|
do {
|
|
let handle = try backgroundTaskExecutorProvider().startBackgroundExecution(callback: callback).get()
|
|
log("Handle is returned, background actions in progress")
|
|
|
|
task.expirationHandler = { [weak self, handle] in
|
|
Task {
|
|
log("Background task expiration handler called")
|
|
self?.backgroundTaskExpired.send()
|
|
await handle.abort(inForeground: false)
|
|
}
|
|
}
|
|
} catch {
|
|
log("Background execution failed to start: \(error.localizedDescription)")
|
|
}
|
|
}
|
|
|
|
private var taskRequest: BGProcessingTaskRequest {
|
|
let request = BGProcessingTaskRequest(identifier: Self.identifier)
|
|
request.requiresExternalPower = false
|
|
request.requiresNetworkConnectivity = true
|
|
request.earliestBeginDate = DateEnvironment.currentDate().thirthyMinutesAfter
|
|
return request
|
|
}
|
|
|
|
private func backgroundExecutionHasCompleted(completionStatus: BackgroundExecutionStatus, task: BackgroundTask) {
|
|
log("Waiting for session to set up. Completion status: \(completionStatus)")
|
|
checkForSessionSetUpToComplete {
|
|
log("Background task finished after the session set up or task expired")
|
|
task.setTaskCompleted(success: true)
|
|
}
|
|
}
|
|
|
|
private func checkForSessionSetUpToComplete(completion: @Sendable @escaping () -> Void) {
|
|
sessionSetUpCheckCancellable =
|
|
Publishers
|
|
.CombineLatest(sessionStatePublisher, timerFactory(0.5))
|
|
.map { sessionState, _ in sessionState }
|
|
.prefix(untilOutputFrom: backgroundTaskExpired)
|
|
.filter { sessionState in sessionState.isAuthorized }
|
|
.first()
|
|
.sink(
|
|
receiveCompletion: { _ in completion() },
|
|
receiveValue: { _ in }
|
|
)
|
|
}
|
|
|
|
}
|
|
|
|
private func log(_ message: String) {
|
|
AppLogger.log(message: message, category: .recurringBackgroundTask)
|
|
}
|
|
|
|
extension Date {
|
|
var thirthyMinutesAfter: Self {
|
|
DateEnvironment.calendar.date(byAdding: .minute, value: 30, to: self).unsafelyUnwrapped
|
|
}
|
|
}
|