Background execution (#100)
* #99 Implement background tasks * #99 Add changelog * #99 Fix some code smells * #99 Use background tasks in room timeline controller * #99 Move background task service into room proxy and media provider
This commit is contained in:
@@ -232,6 +232,12 @@
|
||||
EA31DD9043B91ECB8E45A9A6 /* ScreenshotDetectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F03C9D319676F3C0DC6B0203 /* ScreenshotDetectorTests.swift */; };
|
||||
EA65360A0EC026DD83AC0CF5 /* AuthenticationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA5F386C7701C129398945 /* AuthenticationCoordinator.swift */; };
|
||||
EBD6C79705B3DDB2F7E5F554 /* UserSessionStoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1B52D0ABBA7091A991CAFE /* UserSessionStoreProtocol.swift */; };
|
||||
EC7FFD61286B143500DF372A /* BackgroundTaskServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC7FFD60286B143500DF372A /* BackgroundTaskServiceProtocol.swift */; };
|
||||
EC7FFD63286B14B200DF372A /* BackgroundTaskProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC7FFD62286B14B200DF372A /* BackgroundTaskProtocol.swift */; };
|
||||
EC7FFD65286B15CA00DF372A /* UIKitBackgroundTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC7FFD64286B15CA00DF372A /* UIKitBackgroundTask.swift */; };
|
||||
EC7FFD67286B18B900DF372A /* ApplicationProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC7FFD66286B18B900DF372A /* ApplicationProtocol.swift */; };
|
||||
EC7FFD69286B1D6B00DF372A /* UIKitBackgroundTaskService.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC7FFD68286B1D6B00DF372A /* UIKitBackgroundTaskService.swift */; };
|
||||
EC7FFD6E286B42D000DF372A /* BackgroundTaskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC7FFD6D286B42D000DF372A /* BackgroundTaskTests.swift */; };
|
||||
ED4F663C783E9A8C0E80B983 /* TemplateSimpleScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47543EB19F3DCF308751F53C /* TemplateSimpleScreenViewModel.swift */; };
|
||||
EE8491AD81F47DF3C192497B /* DecorationTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */; };
|
||||
EEC40663922856C65D1E0DF5 /* KeychainControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB9C37196A4C79F24CE80C6 /* KeychainControllerTests.swift */; };
|
||||
@@ -595,6 +601,12 @@
|
||||
E8CA187FE656EE5A3F6C7DE5 /* UIFont+AttributedStringBuilder.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIFont+AttributedStringBuilder.m"; sourceTree = "<group>"; };
|
||||
E9D059BFE329BE09B6D96A9F /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ro; path = ro.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
EBE5502760CF6CA2D7201883 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ja; path = ja.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
EC7FFD60286B143500DF372A /* BackgroundTaskServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundTaskServiceProtocol.swift; sourceTree = "<group>"; };
|
||||
EC7FFD62286B14B200DF372A /* BackgroundTaskProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundTaskProtocol.swift; sourceTree = "<group>"; };
|
||||
EC7FFD64286B15CA00DF372A /* UIKitBackgroundTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitBackgroundTask.swift; sourceTree = "<group>"; };
|
||||
EC7FFD66286B18B900DF372A /* ApplicationProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationProtocol.swift; sourceTree = "<group>"; };
|
||||
EC7FFD68286B1D6B00DF372A /* UIKitBackgroundTaskService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitBackgroundTaskService.swift; sourceTree = "<group>"; };
|
||||
EC7FFD6D286B42D000DF372A /* BackgroundTaskTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundTaskTests.swift; sourceTree = "<group>"; };
|
||||
ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProtocol.swift; sourceTree = "<group>"; };
|
||||
EE8BCD14EFED23459A43FDFF /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
EEE384418EB1FEDFA62C9CD0 /* RoomTimelineViewFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineViewFactoryProtocol.swift; sourceTree = "<group>"; };
|
||||
@@ -691,6 +703,7 @@
|
||||
0787F81684E503024BD0C051 /* Services */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EC7FFD6C286B270500DF372A /* Background */,
|
||||
0ED3F5C21537519389C07644 /* BugReport */,
|
||||
8039515BAA53B7C3275AC64A /* Client */,
|
||||
79E560F5113ED25D172E550C /* Media */,
|
||||
@@ -990,6 +1003,7 @@
|
||||
F03C9D319676F3C0DC6B0203 /* ScreenshotDetectorTests.swift */,
|
||||
3D487C1185D658F8B15B8F55 /* SettingsViewModelTests.swift */,
|
||||
7B5CF94E124616FD89424B73 /* SplashScreenViewModelTests.swift */,
|
||||
EC7FFD6D286B42D000DF372A /* BackgroundTaskTests.swift */,
|
||||
AF552BB969DC98A4BB8CF8D5 /* UserIndicators */,
|
||||
);
|
||||
path = Sources;
|
||||
@@ -1377,6 +1391,18 @@
|
||||
path = Authentication;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EC7FFD6C286B270500DF372A /* Background */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EC7FFD60286B143500DF372A /* BackgroundTaskServiceProtocol.swift */,
|
||||
EC7FFD68286B1D6B00DF372A /* UIKitBackgroundTaskService.swift */,
|
||||
EC7FFD62286B14B200DF372A /* BackgroundTaskProtocol.swift */,
|
||||
EC7FFD64286B15CA00DF372A /* UIKitBackgroundTask.swift */,
|
||||
EC7FFD66286B18B900DF372A /* ApplicationProtocol.swift */,
|
||||
);
|
||||
path = Background;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F8474EB69289112888B65518 /* UserIndicators */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -1715,6 +1741,7 @@
|
||||
7F61F9ACD5EC9E845EF3EFBF /* BugReportServiceTests.swift in Sources */,
|
||||
C7CFDB4929DDD9A3B5BA085D /* BugReportViewModelTests.swift in Sources */,
|
||||
CA1E41AE5CDCB8D801DE0830 /* BuildSettings.swift in Sources */,
|
||||
EC7FFD6E286B42D000DF372A /* BackgroundTaskTests.swift in Sources */,
|
||||
9C45CE85325CD591DADBC4CA /* ElementXTests.swift in Sources */,
|
||||
F6F49E37272AD7397CD29A01 /* HomeScreenViewModelTests.swift in Sources */,
|
||||
0B1F80C2BF7D223159FBA82C /* ImageAnonymizerTests.swift in Sources */,
|
||||
@@ -1805,6 +1832,7 @@
|
||||
BCC3EDB7AD0902797CB4BBC2 /* MXLogger.m in Sources */,
|
||||
F03E16ED043C62FED5A07AE0 /* MatrixEntitityRegex.swift in Sources */,
|
||||
EA1E7949533E19C6D862680A /* MediaProvider.swift in Sources */,
|
||||
EC7FFD67286B18B900DF372A /* ApplicationProtocol.swift in Sources */,
|
||||
7002C55A4C917F3715765127 /* MediaProviderProtocol.swift in Sources */,
|
||||
62BBF5BE7B905222F0477FF2 /* MediaSource.swift in Sources */,
|
||||
03B8FEA668A5B76A93113BB1 /* MemberDetailProviderManager.swift in Sources */,
|
||||
@@ -1849,6 +1877,7 @@
|
||||
1AE4AEA0FA8DEF52671832E0 /* RoomTimelineItemProtocol.swift in Sources */,
|
||||
9BD3A773186291560DF92B62 /* RoomTimelineProvider.swift in Sources */,
|
||||
77D7DAA41AAB36800C1F2E2D /* RoomTimelineProviderProtocol.swift in Sources */,
|
||||
EC7FFD69286B1D6B00DF372A /* UIKitBackgroundTaskService.swift in Sources */,
|
||||
5D430CDE11EAC3E8E6B80A66 /* RoomTimelineViewFactory.swift in Sources */,
|
||||
297CD0A27C87B0C50FF192EE /* RoomTimelineViewFactoryProtocol.swift in Sources */,
|
||||
CF82143AA4A4F7BD11D22946 /* RoomTimelineViewProvider.swift in Sources */,
|
||||
@@ -1872,6 +1901,7 @@
|
||||
5E1FCC43B738941D5A5F1794 /* SplashScreenViewModelProtocol.swift in Sources */,
|
||||
FCB640C576292BEAF7FA3B2E /* SplashViewController.swift in Sources */,
|
||||
B4AAB3257A83B73F53FB2689 /* StateStoreViewModel.swift in Sources */,
|
||||
EC7FFD65286B15CA00DF372A /* UIKitBackgroundTask.swift in Sources */,
|
||||
2F94054F50E312AF30BE07F3 /* String.swift in Sources */,
|
||||
A7D48E44D485B143AADDB77D /* Strings+Untranslated.swift in Sources */,
|
||||
066A1E9B94723EE9F3038044 /* Strings.swift in Sources */,
|
||||
@@ -1897,6 +1927,7 @@
|
||||
0EE5EBA18BA1FE10254BB489 /* UIFont+AttributedStringBuilder.m in Sources */,
|
||||
004561D297DC8B9786AE136F /* UITestScreenIdentifier.swift in Sources */,
|
||||
03CB204C52F18E24A5C3D219 /* UITestsAppCoordinator.swift in Sources */,
|
||||
EC7FFD63286B14B200DF372A /* BackgroundTaskProtocol.swift in Sources */,
|
||||
17CC4FB64F3A670F43ECBE5F /* UITestsRootView.swift in Sources */,
|
||||
8775F46AE3234A5A5688C19D /* UserIndicator.swift in Sources */,
|
||||
7FA4227B2BAAA71560252866 /* UserIndicatorDismissal.swift in Sources */,
|
||||
@@ -1909,6 +1940,7 @@
|
||||
8AB8ED1051216546CB35FA0E /* UserSession.swift in Sources */,
|
||||
978BB24F2A5D31EE59EEC249 /* UserSessionProtocol.swift in Sources */,
|
||||
79A6E08ADE6E7C460A8A17A5 /* UserSessionStore.swift in Sources */,
|
||||
EC7FFD61286B143500DF372A /* BackgroundTaskServiceProtocol.swift in Sources */,
|
||||
EBD6C79705B3DDB2F7E5F554 /* UserSessionStoreProtocol.swift in Sources */,
|
||||
6FC10A00D268FCD48B631E37 /* ViewFrameReader.swift in Sources */,
|
||||
01F4A40C1EDCEC8DC4EC9CFA /* WeakDictionary.swift in Sources */,
|
||||
|
||||
@@ -26,6 +26,7 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
|
||||
|
||||
private let bugReportService: BugReportServiceProtocol
|
||||
private let screenshotDetector: ScreenshotDetector
|
||||
private let backgroundTaskService: BackgroundTaskServiceProtocol
|
||||
|
||||
private var indicatorPresenter: UserIndicatorTypePresenterProtocol
|
||||
private var loadingIndicator: UserIndicator?
|
||||
@@ -58,8 +59,11 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
|
||||
guard let bundleIdentifier = Bundle.main.bundleIdentifier else {
|
||||
fatalError("Should have a valid bundle identifier at this point")
|
||||
}
|
||||
|
||||
backgroundTaskService = UIKitBackgroundTaskService(withApplication: UIApplication.shared)
|
||||
|
||||
userSessionStore = UserSessionStore(bundleIdentifier: bundleIdentifier)
|
||||
userSessionStore = UserSessionStore(bundleIdentifier: bundleIdentifier,
|
||||
backgroundTaskService: backgroundTaskService)
|
||||
|
||||
screenshotDetector = ScreenshotDetector()
|
||||
screenshotDetector.callback = processScreenshotDetection
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// ApplicationProtocol.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Ismail on 28.06.2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
protocol ApplicationProtocol {
|
||||
func beginBackgroundTask(expirationHandler handler: (() -> Void)?) -> UIBackgroundTaskIdentifier
|
||||
|
||||
func beginBackgroundTask(withName taskName: String?, expirationHandler handler: (() -> Void)?) -> UIBackgroundTaskIdentifier
|
||||
|
||||
func endBackgroundTask(_ identifier: UIBackgroundTaskIdentifier)
|
||||
|
||||
var backgroundTimeRemaining: TimeInterval { get }
|
||||
|
||||
var applicationState: UIApplication.State { get }
|
||||
}
|
||||
|
||||
extension UIApplication: ApplicationProtocol {}
|
||||
@@ -0,0 +1,35 @@
|
||||
//
|
||||
// BackgroundTaskProtocol.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Ismail on 28.06.2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
typealias BackgroundTaskExpirationHandler = (BackgroundTaskProtocol) -> Void
|
||||
|
||||
/// BackgroundTaskProtocol is the protocol describing a background task regardless of the platform used.
|
||||
protocol BackgroundTaskProtocol: AnyObject {
|
||||
/// Name of the background task for debug.
|
||||
var name: String { get }
|
||||
|
||||
/// `true` if the background task is currently running.
|
||||
var isRunning: Bool { get }
|
||||
|
||||
/// Flag indicating the background task is reusable. If reusable, `name` is the key to distinguish background tasks.
|
||||
var isReusable: Bool { get }
|
||||
|
||||
/// Elapsed time after the task started. In milliseconds.
|
||||
var elapsedTime: TimeInterval { get }
|
||||
|
||||
/// Expiration handler for the background task
|
||||
var expirationHandler: BackgroundTaskExpirationHandler? { get }
|
||||
|
||||
/// Method to be called when a task reused one more time. Should only be valid for reusable tasks.
|
||||
func reuse()
|
||||
|
||||
/// Stop the background task. Cannot be started anymore. For reusable tasks, should be called same number of times `reuse` called.
|
||||
func stop()
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
//
|
||||
// BackgroundTaskServiceProtocol.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Ismail on 28.06.2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol BackgroundTaskServiceProtocol {
|
||||
|
||||
func startBackgroundTask(withName name: String,
|
||||
isReusable: Bool,
|
||||
expirationHandler: (() -> Void)?) -> BackgroundTaskProtocol?
|
||||
|
||||
}
|
||||
|
||||
extension BackgroundTaskServiceProtocol {
|
||||
|
||||
func startBackgroundTask(withName name: String) -> BackgroundTaskProtocol? {
|
||||
return startBackgroundTask(withName: name,
|
||||
expirationHandler: nil)
|
||||
}
|
||||
|
||||
func startBackgroundTask(withName name: String,
|
||||
isReusable: Bool) -> BackgroundTaskProtocol? {
|
||||
return startBackgroundTask(withName: name,
|
||||
isReusable: isReusable,
|
||||
expirationHandler: nil)
|
||||
}
|
||||
|
||||
func startBackgroundTask(withName name: String,
|
||||
expirationHandler: (() -> Void)?) -> BackgroundTaskProtocol? {
|
||||
return startBackgroundTask(withName: name,
|
||||
isReusable: false,
|
||||
expirationHandler: expirationHandler)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
//
|
||||
// UIKitBackgroundTask.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Ismail on 28.06.2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
/// UIKitBackgroundTask is a concrete implementation of BackgroundTaskProtocol using UIApplication background task.
|
||||
class UIKitBackgroundTask: BackgroundTaskProtocol {
|
||||
let name: String
|
||||
var isRunning: Bool {
|
||||
identifier != .invalid
|
||||
}
|
||||
let isReusable: Bool
|
||||
let expirationHandler: BackgroundTaskExpirationHandler?
|
||||
var elapsedTime: TimeInterval {
|
||||
return Date().timeIntervalSince(startDate) * 1000
|
||||
}
|
||||
|
||||
private let application: ApplicationProtocol
|
||||
private var identifier: UIBackgroundTaskIdentifier = .invalid
|
||||
private var useCounter: Int = 0
|
||||
private let startDate: Date = Date()
|
||||
|
||||
/// Initializes and starts a new background task
|
||||
/// - Parameters:
|
||||
/// - name: name
|
||||
/// - isReusable: flag indicating the task is reusable
|
||||
/// - application: application instance
|
||||
/// - expirationHandler: expiration handler
|
||||
init?(name: String,
|
||||
isReusable: Bool,
|
||||
application: ApplicationProtocol,
|
||||
expirationHandler: BackgroundTaskExpirationHandler?) {
|
||||
self.name = name
|
||||
self.isReusable = isReusable
|
||||
self.application = application
|
||||
self.expirationHandler = expirationHandler
|
||||
|
||||
// attempt to start
|
||||
identifier = application.beginBackgroundTask(withName: name) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.expirationHandler?(self)
|
||||
}
|
||||
|
||||
if identifier == .invalid {
|
||||
MXLog.debug("[UIKitBackgroundTask] Do not start background task: \(name), as OS declined")
|
||||
// call expiration handler immediately
|
||||
expirationHandler?(self)
|
||||
return nil
|
||||
}
|
||||
|
||||
if isReusable {
|
||||
// creation itself is a use
|
||||
reuse()
|
||||
}
|
||||
|
||||
MXLog.debug("[UIKitBackgroundTask] Start background task #\(identifier.rawValue) - \(name)")
|
||||
}
|
||||
|
||||
func reuse() {
|
||||
guard isReusable else {
|
||||
return
|
||||
}
|
||||
useCounter += 1
|
||||
}
|
||||
|
||||
func stop() {
|
||||
if isReusable {
|
||||
useCounter -= 1
|
||||
if useCounter <= 0 {
|
||||
endTask()
|
||||
}
|
||||
} else {
|
||||
endTask()
|
||||
}
|
||||
}
|
||||
|
||||
private func endTask() {
|
||||
if identifier != .invalid {
|
||||
MXLog.debug("[UIKitBackgroundTask] End background task #\(identifier.rawValue) - \(name) after \(readableElapsedTime)")
|
||||
|
||||
application.endBackgroundTask(identifier)
|
||||
identifier = .invalid
|
||||
}
|
||||
}
|
||||
|
||||
private var readableElapsedTime: String {
|
||||
String(format: "%.3fms", elapsedTime)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
//
|
||||
// UIKitBackgroundTaskService.swift
|
||||
// ElementX
|
||||
//
|
||||
// Created by Ismail on 28.06.2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
/// /// UIKitBackgroundTaskService is a concrete implementation of BackgroundTaskServiceProtocol using a given `ApplicationProtocol` instance.
|
||||
class UIKitBackgroundTaskService: BackgroundTaskServiceProtocol {
|
||||
|
||||
private let application: ApplicationProtocol?
|
||||
private var reusableTasks: WeakDictionary<String, UIKitBackgroundTask> = WeakDictionary()
|
||||
|
||||
/// Initializer
|
||||
/// - Parameter application: application instance to use. Defaults to `UIApplication.extensionSafeShared`.
|
||||
init(withApplication application: ApplicationProtocol? = UIApplication.extensionSafeShared) {
|
||||
self.application = application
|
||||
}
|
||||
|
||||
func startBackgroundTask(withName name: String,
|
||||
isReusable: Bool,
|
||||
expirationHandler: (() -> Void)?) -> BackgroundTaskProtocol? {
|
||||
guard let application = application else {
|
||||
MXLog.debug("[UIKitBackgroundTaskService] Do not start background task: \(name). Application is nil")
|
||||
return nil
|
||||
}
|
||||
|
||||
if avoidStartingNewTasks(for: application) {
|
||||
MXLog.debug("[UIKitBackgroundTaskService] Do not start background task: \(name), as not enough time exists")
|
||||
// call expiration handler immediately
|
||||
expirationHandler?()
|
||||
return nil
|
||||
}
|
||||
var created: Bool = false
|
||||
var result: BackgroundTaskProtocol?
|
||||
|
||||
if isReusable {
|
||||
if let oldTask = reusableTasks[name], oldTask.isRunning {
|
||||
oldTask.reuse()
|
||||
result = oldTask
|
||||
} else {
|
||||
if let newTask = UIKitBackgroundTask(name: name,
|
||||
isReusable: isReusable,
|
||||
application: application,
|
||||
expirationHandler: { [weak self] task in
|
||||
guard let self = self else { return }
|
||||
self.reusableTasks[task.name] = nil
|
||||
expirationHandler?()
|
||||
}) {
|
||||
created = true
|
||||
reusableTasks[name] = newTask
|
||||
result = newTask
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let newTask = UIKitBackgroundTask(name: name,
|
||||
isReusable: isReusable,
|
||||
application: application,
|
||||
expirationHandler: { _ in
|
||||
expirationHandler?()
|
||||
}) {
|
||||
result = newTask
|
||||
created = true
|
||||
}
|
||||
}
|
||||
|
||||
let appState = application.applicationState
|
||||
let remainingTime = readableBackgroundTimeRemaining(application.backgroundTimeRemaining)
|
||||
|
||||
MXLog.debug("[UIKitBackgroundTaskService] Background task \(name) \(created ? "started" : "reused") with app state: \(appState) and estimated background time remaining: \(remainingTime)")
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private func readableBackgroundTimeRemaining(_ backgroundTimeRemaining: TimeInterval) -> String {
|
||||
if backgroundTimeRemaining == .greatestFiniteMagnitude {
|
||||
return "undetermined"
|
||||
} else {
|
||||
return String(format: "%.0f seconds", backgroundTimeRemaining)
|
||||
}
|
||||
}
|
||||
|
||||
private func avoidStartingNewTasks(for application: ApplicationProtocol) -> Bool {
|
||||
if application.applicationState == .background
|
||||
&& application.backgroundTimeRemaining < .backgroundTimeRemainingThresholdToStartTasks {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private extension TimeInterval {
|
||||
static let backgroundTimeRemainingThresholdToStartTasks: TimeInterval = 5
|
||||
}
|
||||
|
||||
private extension UIApplication {
|
||||
/// Application instance extension-safe. Will be `nil` on app extensions.
|
||||
static var extensionSafeShared: UIApplication? {
|
||||
let selector = NSSelectorFromString("sharedApplication")
|
||||
guard Self.responds(to: selector) else { return nil }
|
||||
return Self.perform(selector).takeUnretainedValue() as? UIApplication
|
||||
}
|
||||
}
|
||||
|
||||
extension UIApplication.State: CustomStringConvertible {
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .active:
|
||||
return "active"
|
||||
case .inactive:
|
||||
return "inactive"
|
||||
case .background:
|
||||
return "background"
|
||||
@unknown default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ private class WeakClientProxyWrapper: ClientDelegate {
|
||||
class ClientProxy: ClientProxyProtocol {
|
||||
|
||||
private let client: Client
|
||||
private let backgroundTaskService: BackgroundTaskServiceProtocol
|
||||
|
||||
private(set) var rooms: [RoomProxy] = [] {
|
||||
didSet {
|
||||
@@ -39,8 +40,10 @@ class ClientProxy: ClientProxyProtocol {
|
||||
|
||||
let callbacks = PassthroughSubject<ClientProxyCallback, Never>()
|
||||
|
||||
init(client: Client) {
|
||||
init(client: Client,
|
||||
backgroundTaskService: BackgroundTaskServiceProtocol) {
|
||||
self.client = client
|
||||
self.backgroundTaskService = backgroundTaskService
|
||||
|
||||
client.setDelegate(delegate: WeakClientProxyWrapper(clientProxy: self))
|
||||
|
||||
@@ -119,7 +122,9 @@ class ClientProxy: ClientProxyProtocol {
|
||||
MXLog.error("Failed retrieving sdk room with id: \(id)")
|
||||
break
|
||||
}
|
||||
currentRooms.append(RoomProxy(room: sdkRoom, roomMessageFactory: RoomMessageFactory()))
|
||||
currentRooms.append(RoomProxy(room: sdkRoom,
|
||||
roomMessageFactory: RoomMessageFactory(),
|
||||
backgroundTaskService: backgroundTaskService))
|
||||
case .remove(_, let id, _):
|
||||
currentRooms.removeAll { $0.id == id }
|
||||
}
|
||||
|
||||
@@ -12,11 +12,15 @@ import Kingfisher
|
||||
struct MediaProvider: MediaProviderProtocol {
|
||||
private let clientProxy: ClientProxyProtocol
|
||||
private let imageCache: Kingfisher.ImageCache
|
||||
private let backgroundTaskService: BackgroundTaskServiceProtocol
|
||||
private let processingQueue: DispatchQueue
|
||||
|
||||
init(clientProxy: ClientProxyProtocol, imageCache: Kingfisher.ImageCache) {
|
||||
init(clientProxy: ClientProxyProtocol,
|
||||
imageCache: Kingfisher.ImageCache,
|
||||
backgroundTaskService: BackgroundTaskServiceProtocol) {
|
||||
self.clientProxy = clientProxy
|
||||
self.imageCache = imageCache
|
||||
self.backgroundTaskService = backgroundTaskService
|
||||
self.processingQueue = DispatchQueue(label: "MediaProviderProcessingQueue", attributes: .concurrent)
|
||||
}
|
||||
|
||||
@@ -44,6 +48,11 @@ struct MediaProvider: MediaProviderProtocol {
|
||||
if let image = imageFromSource(source) {
|
||||
return .success(image)
|
||||
}
|
||||
|
||||
let loadImageBgTask = backgroundTaskService.startBackgroundTask(withName: "LoadImage: \(source.underlyingSource.url().hashValue)")
|
||||
defer {
|
||||
loadImageBgTask?.stop()
|
||||
}
|
||||
|
||||
let cachedImageLoadResult = await withCheckedContinuation { continuation in
|
||||
imageCache.retrieveImage(forKey: source.underlyingSource.url()) { result in
|
||||
|
||||
@@ -29,8 +29,10 @@ private class WeakRoomProxyWrapper: RoomDelegate {
|
||||
class RoomProxy: RoomProxyProtocol {
|
||||
private let room: Room
|
||||
private let roomMessageFactory: RoomMessageFactoryProtocol
|
||||
private let backgroundTaskService: BackgroundTaskServiceProtocol
|
||||
|
||||
private var backwardStream: BackwardsStreamProtocol?
|
||||
private var sendMessageBgTask: BackgroundTaskProtocol?
|
||||
|
||||
private(set) var displayName: String?
|
||||
|
||||
@@ -38,9 +40,12 @@ class RoomProxy: RoomProxyProtocol {
|
||||
|
||||
private(set) var messages: [RoomMessageProtocol]
|
||||
|
||||
init(room: Room, roomMessageFactory: RoomMessageFactoryProtocol) {
|
||||
init(room: Room,
|
||||
roomMessageFactory: RoomMessageFactoryProtocol,
|
||||
backgroundTaskService: BackgroundTaskServiceProtocol) {
|
||||
self.room = room
|
||||
self.roomMessageFactory = roomMessageFactory
|
||||
self.backgroundTaskService = backgroundTaskService
|
||||
messages = []
|
||||
|
||||
room.setDelegate(delegate: WeakRoomProxyWrapper(roomProxy: self))
|
||||
@@ -157,6 +162,10 @@ class RoomProxy: RoomProxyProtocol {
|
||||
}
|
||||
|
||||
func sendMessage(_ message: String) async -> Result<Void, RoomProxyError> {
|
||||
sendMessageBgTask = backgroundTaskService.startBackgroundTask(withName: "SendMessage", isReusable: true)
|
||||
defer {
|
||||
sendMessageBgTask?.stop()
|
||||
}
|
||||
let messageContent = messageEventContentFromMarkdown(md: message)
|
||||
let transactionId = genTransactionId()
|
||||
|
||||
|
||||
@@ -21,12 +21,14 @@ import Kingfisher
|
||||
class UserSessionStore: UserSessionStoreProtocol {
|
||||
|
||||
private let keychainController: KeychainControllerProtocol
|
||||
private let backgroundTaskService: BackgroundTaskServiceProtocol
|
||||
|
||||
/// Whether or not there are sessions in the store.
|
||||
var hasSessions: Bool { !keychainController.accessTokens().isEmpty }
|
||||
|
||||
init(bundleIdentifier: String) {
|
||||
init(bundleIdentifier: String, backgroundTaskService: BackgroundTaskServiceProtocol) {
|
||||
keychainController = KeychainController(identifier: bundleIdentifier)
|
||||
self.backgroundTaskService = backgroundTaskService
|
||||
}
|
||||
|
||||
func restoreUserSession() async -> Result<UserSession, UserSessionStoreError> {
|
||||
@@ -39,7 +41,9 @@ class UserSessionStore: UserSessionStoreProtocol {
|
||||
switch await restorePreviousLogin(usernameTokenTuple) {
|
||||
case .success(let clientProxy):
|
||||
return .success(UserSession(clientProxy: clientProxy,
|
||||
mediaProvider: MediaProvider(clientProxy: clientProxy, imageCache: ImageCache.default)))
|
||||
mediaProvider: MediaProvider(clientProxy: clientProxy,
|
||||
imageCache: ImageCache.default,
|
||||
backgroundTaskService: backgroundTaskService)))
|
||||
case .failure(let error):
|
||||
MXLog.error("Failed restoring login with error: \(error)")
|
||||
|
||||
@@ -55,7 +59,9 @@ class UserSessionStore: UserSessionStoreProtocol {
|
||||
switch await setupProxyForClient(client) {
|
||||
case .success(let clientProxy):
|
||||
return .success(UserSession(clientProxy: clientProxy,
|
||||
mediaProvider: MediaProvider(clientProxy: clientProxy, imageCache: ImageCache.default)))
|
||||
mediaProvider: MediaProvider(clientProxy: clientProxy,
|
||||
imageCache: ImageCache.default,
|
||||
backgroundTaskService: backgroundTaskService)))
|
||||
case .failure(let error):
|
||||
MXLog.error("Failed creating user session with error: \(error)")
|
||||
return .failure(error)
|
||||
@@ -99,7 +105,7 @@ class UserSessionStore: UserSessionStoreProtocol {
|
||||
return .failure(.failedSettingUpSession)
|
||||
}
|
||||
|
||||
let clientProxy = ClientProxy(client: client)
|
||||
let clientProxy = ClientProxy(client: client, backgroundTaskService: backgroundTaskService)
|
||||
|
||||
return .success(clientProxy)
|
||||
}
|
||||
|
||||
227
UnitTests/Sources/BackgroundTaskTests.swift
Normal file
227
UnitTests/Sources/BackgroundTaskTests.swift
Normal file
@@ -0,0 +1,227 @@
|
||||
//
|
||||
// BackgroundTaskTests.swift
|
||||
// UnitTests
|
||||
//
|
||||
// Created by Ismail on 28.06.2022.
|
||||
// Copyright © 2022 Element. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
@testable import ElementX
|
||||
|
||||
class BackgroundTaskTests: XCTestCase {
|
||||
|
||||
private enum Constants {
|
||||
static let bgTaskName: String = "test"
|
||||
}
|
||||
|
||||
func testInAnExtension() {
|
||||
let service = UIKitBackgroundTaskService(withApplication: nil)
|
||||
let task = service.startBackgroundTask(withName: Constants.bgTaskName)
|
||||
|
||||
XCTAssertNil(task, "Task should not be created")
|
||||
}
|
||||
|
||||
func testInitAndStop() {
|
||||
let service = UIKitBackgroundTaskService(withApplication: UIApplication.mockHealty)
|
||||
guard let task = service.startBackgroundTask(withName: Constants.bgTaskName) else {
|
||||
XCTFail("Failed to setup test conditions")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(task.name, Constants.bgTaskName, "Task name should be persisted")
|
||||
XCTAssertFalse(task.isReusable, "Task should be not reusable by default")
|
||||
XCTAssertTrue(task.isRunning, "Task should be running")
|
||||
|
||||
task.stop()
|
||||
|
||||
XCTAssertFalse(task.isRunning, "Task should be stopped")
|
||||
}
|
||||
|
||||
func testNotReusableInit() {
|
||||
let service = UIKitBackgroundTaskService(withApplication: UIApplication.mockHealty)
|
||||
|
||||
// create two not reusable task with the same name
|
||||
guard let task1 = service.startBackgroundTask(withName: Constants.bgTaskName),
|
||||
let task2 = service.startBackgroundTask(withName: Constants.bgTaskName) else {
|
||||
XCTFail("Failed to setup test conditions")
|
||||
return
|
||||
}
|
||||
|
||||
// task1 & task2 should be different instances
|
||||
XCTAssertFalse(task1 === task2,
|
||||
"Handler should create different tasks when reusability disabled")
|
||||
}
|
||||
|
||||
func testReusableInit() {
|
||||
let service = UIKitBackgroundTaskService(withApplication: UIApplication.mockHealty)
|
||||
|
||||
// create two reusable task with the same name
|
||||
guard let task1 = service.startBackgroundTask(withName: Constants.bgTaskName, isReusable: true),
|
||||
let task2 = service.startBackgroundTask(withName: Constants.bgTaskName, isReusable: true) else {
|
||||
XCTFail("Failed to setup test conditions")
|
||||
return
|
||||
}
|
||||
|
||||
// task1 and task2 should be the same instance
|
||||
XCTAssertTrue(task1 === task2,
|
||||
"Handler should create different tasks when reusability disabled")
|
||||
|
||||
XCTAssertEqual(task1.name, Constants.bgTaskName, "Task name should be persisted")
|
||||
XCTAssertTrue(task1.isReusable, "Task should be reusable")
|
||||
XCTAssertTrue(task1.isRunning, "Task should be running")
|
||||
}
|
||||
|
||||
func testMultipleStops() {
|
||||
let service = UIKitBackgroundTaskService(withApplication: UIApplication.mockHealty)
|
||||
|
||||
// create two reusable task with the same name
|
||||
guard let task = service.startBackgroundTask(withName: Constants.bgTaskName, isReusable: true),
|
||||
service.startBackgroundTask(withName: Constants.bgTaskName, isReusable: true) != nil else {
|
||||
XCTFail("Failed to setup test conditions")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertTrue(task.isRunning, "Task should be running")
|
||||
|
||||
task.stop()
|
||||
|
||||
XCTAssertTrue(task.isRunning, "Task should be still running after one stop call")
|
||||
|
||||
task.stop()
|
||||
|
||||
XCTAssertFalse(task.isRunning, "Task should be stopped after two stop calls")
|
||||
}
|
||||
|
||||
func testNotValidReuse() {
|
||||
let service = UIKitBackgroundTaskService(withApplication: UIApplication.mockHealty)
|
||||
|
||||
// create two reusable task with the same name
|
||||
guard let task = service.startBackgroundTask(withName: Constants.bgTaskName, isReusable: true) else {
|
||||
XCTFail("Failed to setup test conditions")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertTrue(task.isRunning, "Task should be running")
|
||||
|
||||
task.stop()
|
||||
|
||||
XCTAssertFalse(task.isRunning, "Task should be stopped after stop")
|
||||
|
||||
task.reuse()
|
||||
|
||||
XCTAssertFalse(task.isRunning, "Task should be stopped after one stop call, even if reuse is called after")
|
||||
}
|
||||
|
||||
func testValidReuse() {
|
||||
let service = UIKitBackgroundTaskService(withApplication: UIApplication.mockHealty)
|
||||
|
||||
// create two reusable task with the same name
|
||||
guard let task = service.startBackgroundTask(withName: Constants.bgTaskName, isReusable: true) else {
|
||||
XCTFail("Failed to setup test conditions")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertTrue(task.isRunning, "Task should be running")
|
||||
|
||||
task.reuse()
|
||||
|
||||
XCTAssertTrue(task.isRunning, "Task should be still running")
|
||||
|
||||
task.stop()
|
||||
|
||||
XCTAssertTrue(task.isRunning, "Task should be still running after one stop call")
|
||||
|
||||
task.stop()
|
||||
|
||||
XCTAssertFalse(task.isRunning, "Task should be stopped after two stop calls")
|
||||
}
|
||||
|
||||
func testBrokenApp() {
|
||||
let service = UIKitBackgroundTaskService(withApplication: UIApplication.mockBroken)
|
||||
|
||||
// create two reusable task with the same name
|
||||
let task = service.startBackgroundTask(withName: Constants.bgTaskName)
|
||||
|
||||
XCTAssertNil(task, "Task should not be created")
|
||||
}
|
||||
|
||||
func testNoTimeApp() {
|
||||
let service = UIKitBackgroundTaskService(withApplication: UIApplication.mockAboutToSuspend)
|
||||
|
||||
// create two reusable task with the same name
|
||||
let task = service.startBackgroundTask(withName: Constants.bgTaskName)
|
||||
|
||||
XCTAssertNil(task, "Task should not be created")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private extension UIApplication {
|
||||
|
||||
static var mockHealty: ApplicationProtocol {
|
||||
MockApplication()
|
||||
}
|
||||
|
||||
static var mockBroken: ApplicationProtocol {
|
||||
MockApplication(withState: .inactive,
|
||||
backgroundTimeRemaining: 0,
|
||||
allowTasks: false)
|
||||
}
|
||||
|
||||
static var mockAboutToSuspend: ApplicationProtocol {
|
||||
MockApplication(withState: .background,
|
||||
backgroundTimeRemaining: 2,
|
||||
allowTasks: false)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class MockApplication: ApplicationProtocol {
|
||||
|
||||
let applicationState: UIApplication.State
|
||||
let backgroundTimeRemaining: TimeInterval
|
||||
private let allowTasks: Bool
|
||||
|
||||
init(withState applicationState: UIApplication.State = .active,
|
||||
backgroundTimeRemaining: TimeInterval = 10,
|
||||
allowTasks: Bool = true) {
|
||||
self.applicationState = applicationState
|
||||
self.backgroundTimeRemaining = backgroundTimeRemaining
|
||||
self.allowTasks = allowTasks
|
||||
}
|
||||
|
||||
private static var bgTaskIdentifier: Int = 0
|
||||
|
||||
private var bgTasks: [UIBackgroundTaskIdentifier: Bool] = [:]
|
||||
|
||||
func beginBackgroundTask(expirationHandler handler: (() -> Void)?) -> UIBackgroundTaskIdentifier {
|
||||
guard allowTasks else {
|
||||
return .invalid
|
||||
}
|
||||
return beginBackgroundTask(withName: nil, expirationHandler: handler)
|
||||
}
|
||||
|
||||
func beginBackgroundTask(withName taskName: String?, expirationHandler handler: (() -> Void)?) -> UIBackgroundTaskIdentifier {
|
||||
guard allowTasks else {
|
||||
return .invalid
|
||||
}
|
||||
Self.bgTaskIdentifier += 1
|
||||
|
||||
let identifier = UIBackgroundTaskIdentifier(rawValue: Self.bgTaskIdentifier)
|
||||
bgTasks[identifier] = true
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
||||
handler?()
|
||||
}
|
||||
return identifier
|
||||
}
|
||||
|
||||
func endBackgroundTask(_ identifier: UIBackgroundTaskIdentifier) {
|
||||
guard allowTasks else {
|
||||
return
|
||||
}
|
||||
bgTasks.removeValue(forKey: identifier)
|
||||
}
|
||||
|
||||
}
|
||||
1
changelog.d/99.feature
Normal file
1
changelog.d/99.feature
Normal file
@@ -0,0 +1 @@
|
||||
Implement and use background tasks.
|
||||
Reference in New Issue
Block a user