mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 12:07:55 +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:
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user