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:
ismailgulek
2022-06-29 13:03:28 +03:00
committed by GitHub
parent da7cc779fe
commit d89cefa253
13 changed files with 619 additions and 9 deletions

View File

@@ -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 */,

View File

@@ -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

View File

@@ -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 {}

View File

@@ -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()
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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"
}
}
}

View File

@@ -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 }
}

View File

@@ -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

View File

@@ -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()

View File

@@ -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)
}

View 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
View File

@@ -0,0 +1 @@
Implement and use background tasks.