mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 10:33:45 +00:00
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 */; };
|
EA31DD9043B91ECB8E45A9A6 /* ScreenshotDetectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F03C9D319676F3C0DC6B0203 /* ScreenshotDetectorTests.swift */; };
|
||||||
EA65360A0EC026DD83AC0CF5 /* AuthenticationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA5F386C7701C129398945 /* AuthenticationCoordinator.swift */; };
|
EA65360A0EC026DD83AC0CF5 /* AuthenticationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA5F386C7701C129398945 /* AuthenticationCoordinator.swift */; };
|
||||||
EBD6C79705B3DDB2F7E5F554 /* UserSessionStoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1B52D0ABBA7091A991CAFE /* UserSessionStoreProtocol.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 */; };
|
ED4F663C783E9A8C0E80B983 /* TemplateSimpleScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47543EB19F3DCF308751F53C /* TemplateSimpleScreenViewModel.swift */; };
|
||||||
EE8491AD81F47DF3C192497B /* DecorationTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */; };
|
EE8491AD81F47DF3C192497B /* DecorationTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */; };
|
||||||
EEC40663922856C65D1E0DF5 /* KeychainControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB9C37196A4C79F24CE80C6 /* KeychainControllerTests.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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
EEE384418EB1FEDFA62C9CD0 /* RoomTimelineViewFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineViewFactoryProtocol.swift; sourceTree = "<group>"; };
|
||||||
@@ -691,6 +703,7 @@
|
|||||||
0787F81684E503024BD0C051 /* Services */ = {
|
0787F81684E503024BD0C051 /* Services */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
EC7FFD6C286B270500DF372A /* Background */,
|
||||||
0ED3F5C21537519389C07644 /* BugReport */,
|
0ED3F5C21537519389C07644 /* BugReport */,
|
||||||
8039515BAA53B7C3275AC64A /* Client */,
|
8039515BAA53B7C3275AC64A /* Client */,
|
||||||
79E560F5113ED25D172E550C /* Media */,
|
79E560F5113ED25D172E550C /* Media */,
|
||||||
@@ -990,6 +1003,7 @@
|
|||||||
F03C9D319676F3C0DC6B0203 /* ScreenshotDetectorTests.swift */,
|
F03C9D319676F3C0DC6B0203 /* ScreenshotDetectorTests.swift */,
|
||||||
3D487C1185D658F8B15B8F55 /* SettingsViewModelTests.swift */,
|
3D487C1185D658F8B15B8F55 /* SettingsViewModelTests.swift */,
|
||||||
7B5CF94E124616FD89424B73 /* SplashScreenViewModelTests.swift */,
|
7B5CF94E124616FD89424B73 /* SplashScreenViewModelTests.swift */,
|
||||||
|
EC7FFD6D286B42D000DF372A /* BackgroundTaskTests.swift */,
|
||||||
AF552BB969DC98A4BB8CF8D5 /* UserIndicators */,
|
AF552BB969DC98A4BB8CF8D5 /* UserIndicators */,
|
||||||
);
|
);
|
||||||
path = Sources;
|
path = Sources;
|
||||||
@@ -1377,6 +1391,18 @@
|
|||||||
path = Authentication;
|
path = Authentication;
|
||||||
sourceTree = "<group>";
|
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 */ = {
|
F8474EB69289112888B65518 /* UserIndicators */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -1715,6 +1741,7 @@
|
|||||||
7F61F9ACD5EC9E845EF3EFBF /* BugReportServiceTests.swift in Sources */,
|
7F61F9ACD5EC9E845EF3EFBF /* BugReportServiceTests.swift in Sources */,
|
||||||
C7CFDB4929DDD9A3B5BA085D /* BugReportViewModelTests.swift in Sources */,
|
C7CFDB4929DDD9A3B5BA085D /* BugReportViewModelTests.swift in Sources */,
|
||||||
CA1E41AE5CDCB8D801DE0830 /* BuildSettings.swift in Sources */,
|
CA1E41AE5CDCB8D801DE0830 /* BuildSettings.swift in Sources */,
|
||||||
|
EC7FFD6E286B42D000DF372A /* BackgroundTaskTests.swift in Sources */,
|
||||||
9C45CE85325CD591DADBC4CA /* ElementXTests.swift in Sources */,
|
9C45CE85325CD591DADBC4CA /* ElementXTests.swift in Sources */,
|
||||||
F6F49E37272AD7397CD29A01 /* HomeScreenViewModelTests.swift in Sources */,
|
F6F49E37272AD7397CD29A01 /* HomeScreenViewModelTests.swift in Sources */,
|
||||||
0B1F80C2BF7D223159FBA82C /* ImageAnonymizerTests.swift in Sources */,
|
0B1F80C2BF7D223159FBA82C /* ImageAnonymizerTests.swift in Sources */,
|
||||||
@@ -1805,6 +1832,7 @@
|
|||||||
BCC3EDB7AD0902797CB4BBC2 /* MXLogger.m in Sources */,
|
BCC3EDB7AD0902797CB4BBC2 /* MXLogger.m in Sources */,
|
||||||
F03E16ED043C62FED5A07AE0 /* MatrixEntitityRegex.swift in Sources */,
|
F03E16ED043C62FED5A07AE0 /* MatrixEntitityRegex.swift in Sources */,
|
||||||
EA1E7949533E19C6D862680A /* MediaProvider.swift in Sources */,
|
EA1E7949533E19C6D862680A /* MediaProvider.swift in Sources */,
|
||||||
|
EC7FFD67286B18B900DF372A /* ApplicationProtocol.swift in Sources */,
|
||||||
7002C55A4C917F3715765127 /* MediaProviderProtocol.swift in Sources */,
|
7002C55A4C917F3715765127 /* MediaProviderProtocol.swift in Sources */,
|
||||||
62BBF5BE7B905222F0477FF2 /* MediaSource.swift in Sources */,
|
62BBF5BE7B905222F0477FF2 /* MediaSource.swift in Sources */,
|
||||||
03B8FEA668A5B76A93113BB1 /* MemberDetailProviderManager.swift in Sources */,
|
03B8FEA668A5B76A93113BB1 /* MemberDetailProviderManager.swift in Sources */,
|
||||||
@@ -1849,6 +1877,7 @@
|
|||||||
1AE4AEA0FA8DEF52671832E0 /* RoomTimelineItemProtocol.swift in Sources */,
|
1AE4AEA0FA8DEF52671832E0 /* RoomTimelineItemProtocol.swift in Sources */,
|
||||||
9BD3A773186291560DF92B62 /* RoomTimelineProvider.swift in Sources */,
|
9BD3A773186291560DF92B62 /* RoomTimelineProvider.swift in Sources */,
|
||||||
77D7DAA41AAB36800C1F2E2D /* RoomTimelineProviderProtocol.swift in Sources */,
|
77D7DAA41AAB36800C1F2E2D /* RoomTimelineProviderProtocol.swift in Sources */,
|
||||||
|
EC7FFD69286B1D6B00DF372A /* UIKitBackgroundTaskService.swift in Sources */,
|
||||||
5D430CDE11EAC3E8E6B80A66 /* RoomTimelineViewFactory.swift in Sources */,
|
5D430CDE11EAC3E8E6B80A66 /* RoomTimelineViewFactory.swift in Sources */,
|
||||||
297CD0A27C87B0C50FF192EE /* RoomTimelineViewFactoryProtocol.swift in Sources */,
|
297CD0A27C87B0C50FF192EE /* RoomTimelineViewFactoryProtocol.swift in Sources */,
|
||||||
CF82143AA4A4F7BD11D22946 /* RoomTimelineViewProvider.swift in Sources */,
|
CF82143AA4A4F7BD11D22946 /* RoomTimelineViewProvider.swift in Sources */,
|
||||||
@@ -1872,6 +1901,7 @@
|
|||||||
5E1FCC43B738941D5A5F1794 /* SplashScreenViewModelProtocol.swift in Sources */,
|
5E1FCC43B738941D5A5F1794 /* SplashScreenViewModelProtocol.swift in Sources */,
|
||||||
FCB640C576292BEAF7FA3B2E /* SplashViewController.swift in Sources */,
|
FCB640C576292BEAF7FA3B2E /* SplashViewController.swift in Sources */,
|
||||||
B4AAB3257A83B73F53FB2689 /* StateStoreViewModel.swift in Sources */,
|
B4AAB3257A83B73F53FB2689 /* StateStoreViewModel.swift in Sources */,
|
||||||
|
EC7FFD65286B15CA00DF372A /* UIKitBackgroundTask.swift in Sources */,
|
||||||
2F94054F50E312AF30BE07F3 /* String.swift in Sources */,
|
2F94054F50E312AF30BE07F3 /* String.swift in Sources */,
|
||||||
A7D48E44D485B143AADDB77D /* Strings+Untranslated.swift in Sources */,
|
A7D48E44D485B143AADDB77D /* Strings+Untranslated.swift in Sources */,
|
||||||
066A1E9B94723EE9F3038044 /* Strings.swift in Sources */,
|
066A1E9B94723EE9F3038044 /* Strings.swift in Sources */,
|
||||||
@@ -1897,6 +1927,7 @@
|
|||||||
0EE5EBA18BA1FE10254BB489 /* UIFont+AttributedStringBuilder.m in Sources */,
|
0EE5EBA18BA1FE10254BB489 /* UIFont+AttributedStringBuilder.m in Sources */,
|
||||||
004561D297DC8B9786AE136F /* UITestScreenIdentifier.swift in Sources */,
|
004561D297DC8B9786AE136F /* UITestScreenIdentifier.swift in Sources */,
|
||||||
03CB204C52F18E24A5C3D219 /* UITestsAppCoordinator.swift in Sources */,
|
03CB204C52F18E24A5C3D219 /* UITestsAppCoordinator.swift in Sources */,
|
||||||
|
EC7FFD63286B14B200DF372A /* BackgroundTaskProtocol.swift in Sources */,
|
||||||
17CC4FB64F3A670F43ECBE5F /* UITestsRootView.swift in Sources */,
|
17CC4FB64F3A670F43ECBE5F /* UITestsRootView.swift in Sources */,
|
||||||
8775F46AE3234A5A5688C19D /* UserIndicator.swift in Sources */,
|
8775F46AE3234A5A5688C19D /* UserIndicator.swift in Sources */,
|
||||||
7FA4227B2BAAA71560252866 /* UserIndicatorDismissal.swift in Sources */,
|
7FA4227B2BAAA71560252866 /* UserIndicatorDismissal.swift in Sources */,
|
||||||
@@ -1909,6 +1940,7 @@
|
|||||||
8AB8ED1051216546CB35FA0E /* UserSession.swift in Sources */,
|
8AB8ED1051216546CB35FA0E /* UserSession.swift in Sources */,
|
||||||
978BB24F2A5D31EE59EEC249 /* UserSessionProtocol.swift in Sources */,
|
978BB24F2A5D31EE59EEC249 /* UserSessionProtocol.swift in Sources */,
|
||||||
79A6E08ADE6E7C460A8A17A5 /* UserSessionStore.swift in Sources */,
|
79A6E08ADE6E7C460A8A17A5 /* UserSessionStore.swift in Sources */,
|
||||||
|
EC7FFD61286B143500DF372A /* BackgroundTaskServiceProtocol.swift in Sources */,
|
||||||
EBD6C79705B3DDB2F7E5F554 /* UserSessionStoreProtocol.swift in Sources */,
|
EBD6C79705B3DDB2F7E5F554 /* UserSessionStoreProtocol.swift in Sources */,
|
||||||
6FC10A00D268FCD48B631E37 /* ViewFrameReader.swift in Sources */,
|
6FC10A00D268FCD48B631E37 /* ViewFrameReader.swift in Sources */,
|
||||||
01F4A40C1EDCEC8DC4EC9CFA /* WeakDictionary.swift in Sources */,
|
01F4A40C1EDCEC8DC4EC9CFA /* WeakDictionary.swift in Sources */,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
|
|||||||
|
|
||||||
private let bugReportService: BugReportServiceProtocol
|
private let bugReportService: BugReportServiceProtocol
|
||||||
private let screenshotDetector: ScreenshotDetector
|
private let screenshotDetector: ScreenshotDetector
|
||||||
|
private let backgroundTaskService: BackgroundTaskServiceProtocol
|
||||||
|
|
||||||
private var indicatorPresenter: UserIndicatorTypePresenterProtocol
|
private var indicatorPresenter: UserIndicatorTypePresenterProtocol
|
||||||
private var loadingIndicator: UserIndicator?
|
private var loadingIndicator: UserIndicator?
|
||||||
@@ -58,8 +59,11 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
|
|||||||
guard let bundleIdentifier = Bundle.main.bundleIdentifier else {
|
guard let bundleIdentifier = Bundle.main.bundleIdentifier else {
|
||||||
fatalError("Should have a valid bundle identifier at this point")
|
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 = ScreenshotDetector()
|
||||||
screenshotDetector.callback = processScreenshotDetection
|
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 {
|
class ClientProxy: ClientProxyProtocol {
|
||||||
|
|
||||||
private let client: Client
|
private let client: Client
|
||||||
|
private let backgroundTaskService: BackgroundTaskServiceProtocol
|
||||||
|
|
||||||
private(set) var rooms: [RoomProxy] = [] {
|
private(set) var rooms: [RoomProxy] = [] {
|
||||||
didSet {
|
didSet {
|
||||||
@@ -39,8 +40,10 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
|
|
||||||
let callbacks = PassthroughSubject<ClientProxyCallback, Never>()
|
let callbacks = PassthroughSubject<ClientProxyCallback, Never>()
|
||||||
|
|
||||||
init(client: Client) {
|
init(client: Client,
|
||||||
|
backgroundTaskService: BackgroundTaskServiceProtocol) {
|
||||||
self.client = client
|
self.client = client
|
||||||
|
self.backgroundTaskService = backgroundTaskService
|
||||||
|
|
||||||
client.setDelegate(delegate: WeakClientProxyWrapper(clientProxy: self))
|
client.setDelegate(delegate: WeakClientProxyWrapper(clientProxy: self))
|
||||||
|
|
||||||
@@ -119,7 +122,9 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
MXLog.error("Failed retrieving sdk room with id: \(id)")
|
MXLog.error("Failed retrieving sdk room with id: \(id)")
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
currentRooms.append(RoomProxy(room: sdkRoom, roomMessageFactory: RoomMessageFactory()))
|
currentRooms.append(RoomProxy(room: sdkRoom,
|
||||||
|
roomMessageFactory: RoomMessageFactory(),
|
||||||
|
backgroundTaskService: backgroundTaskService))
|
||||||
case .remove(_, let id, _):
|
case .remove(_, let id, _):
|
||||||
currentRooms.removeAll { $0.id == id }
|
currentRooms.removeAll { $0.id == id }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,11 +12,15 @@ import Kingfisher
|
|||||||
struct MediaProvider: MediaProviderProtocol {
|
struct MediaProvider: MediaProviderProtocol {
|
||||||
private let clientProxy: ClientProxyProtocol
|
private let clientProxy: ClientProxyProtocol
|
||||||
private let imageCache: Kingfisher.ImageCache
|
private let imageCache: Kingfisher.ImageCache
|
||||||
|
private let backgroundTaskService: BackgroundTaskServiceProtocol
|
||||||
private let processingQueue: DispatchQueue
|
private let processingQueue: DispatchQueue
|
||||||
|
|
||||||
init(clientProxy: ClientProxyProtocol, imageCache: Kingfisher.ImageCache) {
|
init(clientProxy: ClientProxyProtocol,
|
||||||
|
imageCache: Kingfisher.ImageCache,
|
||||||
|
backgroundTaskService: BackgroundTaskServiceProtocol) {
|
||||||
self.clientProxy = clientProxy
|
self.clientProxy = clientProxy
|
||||||
self.imageCache = imageCache
|
self.imageCache = imageCache
|
||||||
|
self.backgroundTaskService = backgroundTaskService
|
||||||
self.processingQueue = DispatchQueue(label: "MediaProviderProcessingQueue", attributes: .concurrent)
|
self.processingQueue = DispatchQueue(label: "MediaProviderProcessingQueue", attributes: .concurrent)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,6 +48,11 @@ struct MediaProvider: MediaProviderProtocol {
|
|||||||
if let image = imageFromSource(source) {
|
if let image = imageFromSource(source) {
|
||||||
return .success(image)
|
return .success(image)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let loadImageBgTask = backgroundTaskService.startBackgroundTask(withName: "LoadImage: \(source.underlyingSource.url().hashValue)")
|
||||||
|
defer {
|
||||||
|
loadImageBgTask?.stop()
|
||||||
|
}
|
||||||
|
|
||||||
let cachedImageLoadResult = await withCheckedContinuation { continuation in
|
let cachedImageLoadResult = await withCheckedContinuation { continuation in
|
||||||
imageCache.retrieveImage(forKey: source.underlyingSource.url()) { result in
|
imageCache.retrieveImage(forKey: source.underlyingSource.url()) { result in
|
||||||
|
|||||||
@@ -29,8 +29,10 @@ private class WeakRoomProxyWrapper: RoomDelegate {
|
|||||||
class RoomProxy: RoomProxyProtocol {
|
class RoomProxy: RoomProxyProtocol {
|
||||||
private let room: Room
|
private let room: Room
|
||||||
private let roomMessageFactory: RoomMessageFactoryProtocol
|
private let roomMessageFactory: RoomMessageFactoryProtocol
|
||||||
|
private let backgroundTaskService: BackgroundTaskServiceProtocol
|
||||||
|
|
||||||
private var backwardStream: BackwardsStreamProtocol?
|
private var backwardStream: BackwardsStreamProtocol?
|
||||||
|
private var sendMessageBgTask: BackgroundTaskProtocol?
|
||||||
|
|
||||||
private(set) var displayName: String?
|
private(set) var displayName: String?
|
||||||
|
|
||||||
@@ -38,9 +40,12 @@ class RoomProxy: RoomProxyProtocol {
|
|||||||
|
|
||||||
private(set) var messages: [RoomMessageProtocol]
|
private(set) var messages: [RoomMessageProtocol]
|
||||||
|
|
||||||
init(room: Room, roomMessageFactory: RoomMessageFactoryProtocol) {
|
init(room: Room,
|
||||||
|
roomMessageFactory: RoomMessageFactoryProtocol,
|
||||||
|
backgroundTaskService: BackgroundTaskServiceProtocol) {
|
||||||
self.room = room
|
self.room = room
|
||||||
self.roomMessageFactory = roomMessageFactory
|
self.roomMessageFactory = roomMessageFactory
|
||||||
|
self.backgroundTaskService = backgroundTaskService
|
||||||
messages = []
|
messages = []
|
||||||
|
|
||||||
room.setDelegate(delegate: WeakRoomProxyWrapper(roomProxy: self))
|
room.setDelegate(delegate: WeakRoomProxyWrapper(roomProxy: self))
|
||||||
@@ -157,6 +162,10 @@ class RoomProxy: RoomProxyProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func sendMessage(_ message: String) async -> Result<Void, RoomProxyError> {
|
func sendMessage(_ message: String) async -> Result<Void, RoomProxyError> {
|
||||||
|
sendMessageBgTask = backgroundTaskService.startBackgroundTask(withName: "SendMessage", isReusable: true)
|
||||||
|
defer {
|
||||||
|
sendMessageBgTask?.stop()
|
||||||
|
}
|
||||||
let messageContent = messageEventContentFromMarkdown(md: message)
|
let messageContent = messageEventContentFromMarkdown(md: message)
|
||||||
let transactionId = genTransactionId()
|
let transactionId = genTransactionId()
|
||||||
|
|
||||||
|
|||||||
@@ -21,12 +21,14 @@ import Kingfisher
|
|||||||
class UserSessionStore: UserSessionStoreProtocol {
|
class UserSessionStore: UserSessionStoreProtocol {
|
||||||
|
|
||||||
private let keychainController: KeychainControllerProtocol
|
private let keychainController: KeychainControllerProtocol
|
||||||
|
private let backgroundTaskService: BackgroundTaskServiceProtocol
|
||||||
|
|
||||||
/// Whether or not there are sessions in the store.
|
/// Whether or not there are sessions in the store.
|
||||||
var hasSessions: Bool { !keychainController.accessTokens().isEmpty }
|
var hasSessions: Bool { !keychainController.accessTokens().isEmpty }
|
||||||
|
|
||||||
init(bundleIdentifier: String) {
|
init(bundleIdentifier: String, backgroundTaskService: BackgroundTaskServiceProtocol) {
|
||||||
keychainController = KeychainController(identifier: bundleIdentifier)
|
keychainController = KeychainController(identifier: bundleIdentifier)
|
||||||
|
self.backgroundTaskService = backgroundTaskService
|
||||||
}
|
}
|
||||||
|
|
||||||
func restoreUserSession() async -> Result<UserSession, UserSessionStoreError> {
|
func restoreUserSession() async -> Result<UserSession, UserSessionStoreError> {
|
||||||
@@ -39,7 +41,9 @@ class UserSessionStore: UserSessionStoreProtocol {
|
|||||||
switch await restorePreviousLogin(usernameTokenTuple) {
|
switch await restorePreviousLogin(usernameTokenTuple) {
|
||||||
case .success(let clientProxy):
|
case .success(let clientProxy):
|
||||||
return .success(UserSession(clientProxy: 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):
|
case .failure(let error):
|
||||||
MXLog.error("Failed restoring login with error: \(error)")
|
MXLog.error("Failed restoring login with error: \(error)")
|
||||||
|
|
||||||
@@ -55,7 +59,9 @@ class UserSessionStore: UserSessionStoreProtocol {
|
|||||||
switch await setupProxyForClient(client) {
|
switch await setupProxyForClient(client) {
|
||||||
case .success(let clientProxy):
|
case .success(let clientProxy):
|
||||||
return .success(UserSession(clientProxy: 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):
|
case .failure(let error):
|
||||||
MXLog.error("Failed creating user session with error: \(error)")
|
MXLog.error("Failed creating user session with error: \(error)")
|
||||||
return .failure(error)
|
return .failure(error)
|
||||||
@@ -99,7 +105,7 @@ class UserSessionStore: UserSessionStoreProtocol {
|
|||||||
return .failure(.failedSettingUpSession)
|
return .failure(.failedSettingUpSession)
|
||||||
}
|
}
|
||||||
|
|
||||||
let clientProxy = ClientProxy(client: client)
|
let clientProxy = ClientProxy(client: client, backgroundTaskService: backgroundTaskService)
|
||||||
|
|
||||||
return .success(clientProxy)
|
return .success(clientProxy)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Implement and use background tasks.
|
||||||
Reference in New Issue
Block a user