Introduce an AppMediator which will:

- combine multiple services to handle app wide requests
- remove direct UIApplication access from everywhere
- fix #2703 by combining UIApplication.applicationState with the WindowManager's main window appearance
- leave the door open for more app control in the future (especiall for the mac)
This commit is contained in:
Stefan Ceriu
2024-04-19 13:59:15 +03:00
committed by Stefan Ceriu
parent bdacea10a2
commit 9a172df7fd
30 changed files with 261 additions and 202 deletions
@@ -1,46 +0,0 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
import UIKit
// sourcery: AutoMockable
protocol ApplicationProtocol {
func beginBackgroundTask(withName taskName: String?, expirationHandler handler: (() -> Void)?) -> UIBackgroundTaskIdentifier
func endBackgroundTask(_ identifier: UIBackgroundTaskIdentifier)
func open(_ url: URL)
func openAppSettings()
var backgroundTimeRemaining: TimeInterval { get }
var applicationState: UIApplication.State { get }
}
extension UIApplication: ApplicationProtocol {
func open(_ url: URL) {
open(url, options: [:], completionHandler: nil)
}
func openAppSettings() {
guard let url = URL(string: UIApplication.openSettingsURLString) else {
return
}
open(url)
}
}
@@ -17,7 +17,6 @@
import Foundation
import UIKit
/// UIKitBackgroundTask is a concrete implementation of BackgroundTaskProtocol using UIApplication background task.
class UIKitBackgroundTask: BackgroundTaskProtocol {
let name: String
var isRunning: Bool {
@@ -30,28 +29,21 @@ class UIKitBackgroundTask: BackgroundTaskProtocol {
Date().timeIntervalSince(startDate) * 1000
}
private let application: ApplicationProtocol
private let appMediator: AppMediatorProtocol
private var identifier: UIBackgroundTaskIdentifier = .invalid
private var useCounter = 0
private let startDate = 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,
appMediator: AppMediatorProtocol,
expirationHandler: BackgroundTaskExpirationHandler?) {
self.name = name
self.isReusable = isReusable
self.application = application
self.appMediator = appMediator
self.expirationHandler = expirationHandler
// attempt to start
identifier = application.beginBackgroundTask(withName: name) { [weak self] in
identifier = appMediator.beginBackgroundTask(withName: name) { [weak self] in
guard let self else { return }
self.stop()
self.expirationHandler?(self)
@@ -59,13 +51,11 @@ class UIKitBackgroundTask: BackgroundTaskProtocol {
if identifier == .invalid {
MXLog.error("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()
}
@@ -94,7 +84,7 @@ class UIKitBackgroundTask: BackgroundTaskProtocol {
if identifier != .invalid {
MXLog.verbose("End background task #\(identifier.rawValue) - \(name) after \(readableElapsedTime)")
application.endBackgroundTask(identifier)
appMediator.endBackgroundTask(identifier)
identifier = .invalid
}
}
@@ -19,28 +19,17 @@ import UIKit
/// /// UIKitBackgroundTaskService is a concrete implementation of BackgroundTaskServiceProtocol using a given `ApplicationProtocol` instance.
class UIKitBackgroundTaskService: BackgroundTaskServiceProtocol {
private let applicationBlock: () -> ApplicationProtocol?
private let appMediator: AppMediatorProtocol
private var reusableTasks = NSMapTable<NSString, UIKitBackgroundTask>(keyOptions: .strongMemory, valueOptions: .weakMemory)
private var application: ApplicationProtocol? {
applicationBlock()
}
/// Initializer
/// - Parameter applicationBlock: block returning the application instance to use. Defaults to a block returning `UIApplication.extensionSafeShared`.
init(withApplicationBlock applicationBlock: @escaping () -> ApplicationProtocol? = { UIApplication.extensionSafeShared }) {
self.applicationBlock = applicationBlock
init(appMediator: AppMediatorProtocol) {
self.appMediator = appMediator
}
func startBackgroundTask(withName name: String,
isReusable: Bool,
expirationHandler: (() -> Void)?) -> BackgroundTaskProtocol? {
guard let application else {
MXLog.error("Do not start background task: \(name). Application is nil")
return nil
}
if avoidStartingNewTasks(for: application) {
if shouldAvoidStartingNewTasks() {
MXLog.error("Do not start background task: \(name), as not enough time exists")
// call expiration handler immediately
expirationHandler?()
@@ -56,7 +45,7 @@ class UIKitBackgroundTaskService: BackgroundTaskServiceProtocol {
} else {
if let newTask = UIKitBackgroundTask(name: name,
isReusable: isReusable,
application: application,
appMediator: appMediator,
expirationHandler: { [weak self] task in
guard let self else { return }
self.reusableTasks.removeObject(forKey: task.name as NSString)
@@ -70,7 +59,7 @@ class UIKitBackgroundTaskService: BackgroundTaskServiceProtocol {
} else {
if let newTask = UIKitBackgroundTask(name: name,
isReusable: isReusable,
application: application,
appMediator: appMediator,
expirationHandler: { _ in
expirationHandler?()
}) {
@@ -79,8 +68,8 @@ class UIKitBackgroundTaskService: BackgroundTaskServiceProtocol {
}
}
let appState = application.applicationState
let remainingTime = readableBackgroundTimeRemaining(application.backgroundTimeRemaining)
let appState = appMediator.appState
let remainingTime = readableBackgroundTimeRemaining(appMediator.backgroundTimeRemaining)
MXLog.verbose("Background task \(name) \(created ? "started" : "reused") with app state: \(appState) and estimated background time remaining: \(remainingTime)")
@@ -95,9 +84,9 @@ class UIKitBackgroundTaskService: BackgroundTaskServiceProtocol {
}
}
private func avoidStartingNewTasks(for application: ApplicationProtocol) -> Bool {
if application.applicationState == .background,
application.backgroundTimeRemaining < .backgroundTimeRemainingThresholdToStartTasks {
private func shouldAvoidStartingNewTasks() -> Bool {
if appMediator.appState == .background,
appMediator.backgroundTimeRemaining < .backgroundTimeRemainingThresholdToStartTasks {
return true
}
return false
@@ -107,27 +96,3 @@ class UIKitBackgroundTaskService: BackgroundTaskServiceProtocol {
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 responds(to: selector) else { return nil }
return 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"
}
}
}