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
@@ -24,6 +24,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
private let stateMachine: AppCoordinatorStateMachine
private let navigationRootCoordinator: NavigationRootCoordinator
private let userSessionStore: UserSessionStoreProtocol
private let appMediator: AppMediator
private let appSettings: AppSettings
private let appDelegate: AppDelegate
@@ -65,6 +66,8 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
init(appDelegate: AppDelegate) {
windowManager = WindowManager(appDelegate: appDelegate)
appMediator = AppMediator(windowManager: windowManager)
Self.setupEnvironmentVariables()
let appSettings = AppSettings()
@@ -100,9 +103,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
navigationRootCoordinator.setRootCoordinator(SplashScreenCoordinator())
backgroundTaskService = UIKitBackgroundTaskService {
UIApplication.shared
}
backgroundTaskService = UIKitBackgroundTaskService(appMediator: appMediator)
let keychainController = KeychainController(service: .sessions,
accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier)
@@ -409,6 +410,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
authenticationFlowCoordinator = AuthenticationFlowCoordinator(authenticationService: authenticationService,
bugReportService: ServiceLocator.shared.bugReportService,
navigationRootCoordinator: navigationRootCoordinator,
appMediator: appMediator,
appSettings: appSettings,
analytics: ServiceLocator.shared.analytics,
userIndicatorController: ServiceLocator.shared.userIndicatorController,
@@ -471,6 +473,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
appLockService: appLockFlowCoordinator.appLockService,
bugReportService: ServiceLocator.shared.bugReportService,
roomTimelineControllerFactory: RoomTimelineControllerFactory(),
appMediator: appMediator,
appSettings: appSettings,
analytics: ServiceLocator.shared.analytics,
notificationManager: notificationManager,
@@ -645,7 +648,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
private func handleAppRoute(_ appRoute: AppRoute) {
if let userSessionFlowCoordinator {
userSessionFlowCoordinator.handleAppRoute(appRoute, animated: UIApplication.shared.applicationState == .active)
userSessionFlowCoordinator.handleAppRoute(appRoute, animated: appMediator.appState == .active)
} else {
storedAppRoute = appRoute
}
@@ -0,0 +1,68 @@
//
// Copyright 2024 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 UIKit
class AppMediator: AppMediatorProtocol {
private let windowManager: WindowManagerProtocol
// UIApplication.State won't update if we store this e.g. in the constructor
private var application: UIApplication {
UIApplication.shared
}
init(windowManager: WindowManagerProtocol) {
self.windowManager = windowManager
}
func beginBackgroundTask(withName taskName: String?, expirationHandler handler: (() -> Void)?) -> UIBackgroundTaskIdentifier {
application.beginBackgroundTask(withName: taskName, expirationHandler: handler)
}
func endBackgroundTask(_ identifier: UIBackgroundTaskIdentifier) {
application.endBackgroundTask(identifier)
}
func open(_ url: URL) {
application.open(url, options: [:], completionHandler: nil)
}
func openAppSettings() {
guard let url = URL(string: UIApplication.openSettingsURLString) else {
return
}
open(url)
}
var backgroundTimeRemaining: TimeInterval {
application.backgroundTimeRemaining
}
@MainActor
var appState: UIApplication.State {
switch application.applicationState {
case .active:
windowManager.mainWindow.traitCollection.activeAppearance == .active ? .active : .inactive
case .inactive:
.inactive
case .background:
.background
default:
.inactive
}
}
}
@@ -18,7 +18,7 @@ import Foundation
import UIKit
// sourcery: AutoMockable
protocol ApplicationProtocol {
protocol AppMediatorProtocol {
func beginBackgroundTask(withName taskName: String?, expirationHandler handler: (() -> Void)?) -> UIBackgroundTaskIdentifier
func endBackgroundTask(_ identifier: UIBackgroundTaskIdentifier)
@@ -29,18 +29,35 @@ protocol ApplicationProtocol {
var backgroundTimeRemaining: TimeInterval { get }
var applicationState: UIApplication.State { get }
var appState: 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
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"
}
}
}
extension UIUserInterfaceActiveAppearance: CustomStringConvertible {
public var description: String {
switch self {
case .active:
return "active"
case .inactive:
return "inactive"
case .unspecified:
return "unspecified"
@unknown default:
return "unknown"
}
open(url)
}
}
@@ -27,6 +27,7 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
private let bugReportService: BugReportServiceProtocol
private let navigationRootCoordinator: NavigationRootCoordinator
private let navigationStackCoordinator: NavigationStackCoordinator
private let appMediator: AppMediatorProtocol
private let appSettings: AppSettings
private let analytics: AnalyticsService
private let userIndicatorController: UserIndicatorControllerProtocol
@@ -44,6 +45,7 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
init(authenticationService: AuthenticationServiceProxyProtocol,
bugReportService: BugReportServiceProtocol,
navigationRootCoordinator: NavigationRootCoordinator,
appMediator: AppMediatorProtocol,
appSettings: AppSettings,
analytics: AnalyticsService,
userIndicatorController: UserIndicatorControllerProtocol,
@@ -51,6 +53,7 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
self.authenticationService = authenticationService
self.bugReportService = bugReportService
self.navigationRootCoordinator = navigationRootCoordinator
self.appMediator = appMediator
self.appSettings = appSettings
self.analytics = analytics
self.userIndicatorController = userIndicatorController
@@ -107,7 +110,8 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol {
private func startQRCodeLogin() {
let coordinator = QRCodeLoginScreenCoordinator(parameters: .init(qrCodeLoginService: QRCodeLoginService(),
orientationManager: orientationManager))
orientationManager: orientationManager,
appMediator: appMediator))
coordinator.actionsPublisher.sink { [weak self] action in
guard let self else {
return
@@ -46,6 +46,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
private let roomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol
private let navigationStackCoordinator: NavigationStackCoordinator
private let emojiProvider: EmojiProviderProtocol
private let appMediator: AppMediatorProtocol
private let appSettings: AppSettings
private let analytics: AnalyticsService
private let userIndicatorController: UserIndicatorControllerProtocol
@@ -73,6 +74,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
roomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol,
navigationStackCoordinator: NavigationStackCoordinator,
emojiProvider: EmojiProviderProtocol,
appMediator: AppMediatorProtocol,
appSettings: AppSettings,
analytics: AnalyticsService,
userIndicatorController: UserIndicatorControllerProtocol,
@@ -83,6 +85,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
self.roomTimelineControllerFactory = roomTimelineControllerFactory
self.navigationStackCoordinator = navigationStackCoordinator
self.emojiProvider = emojiProvider
self.appMediator = appMediator
self.appSettings = appSettings
self.analytics = analytics
self.userIndicatorController = userIndicatorController
@@ -439,7 +442,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
voiceMessageMediaManager: userSession.voiceMessageMediaManager,
emojiProvider: emojiProvider,
completionSuggestionService: completionSuggestionService,
appMediator: appMediator,
appSettings: appSettings)
let coordinator = RoomScreenCoordinator(parameters: parameters)
coordinator.actions
.sink { [weak self] action in
@@ -714,7 +719,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
private func presentMapNavigator(interactionMode: StaticLocationInteractionMode) {
let stackCoordinator = NavigationStackCoordinator()
let params = StaticLocationScreenCoordinatorParameters(interactionMode: interactionMode)
let params = StaticLocationScreenCoordinatorParameters(interactionMode: interactionMode, appMediator: appMediator)
let coordinator = StaticLocationScreenCoordinator(parameters: params)
coordinator.actions.sink { [weak self] action in
@@ -1126,6 +1131,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
roomTimelineControllerFactory: roomTimelineControllerFactory,
navigationStackCoordinator: navigationStackCoordinator,
emojiProvider: emojiProvider,
appMediator: appMediator,
appSettings: appSettings,
analytics: analytics,
userIndicatorController: userIndicatorController,
@@ -30,6 +30,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
private let navigationSplitCoordinator: NavigationSplitCoordinator
private let windowManager: WindowManagerProtocol
private let bugReportService: BugReportServiceProtocol
private let appMediator: AppMediatorProtocol
private let appSettings: AppSettings
private let analytics: AnalyticsService
private let notificationManager: NotificationManagerProtocol
@@ -71,6 +72,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
appLockService: AppLockServiceProtocol,
bugReportService: BugReportServiceProtocol,
roomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol,
appMediator: AppMediatorProtocol,
appSettings: AppSettings,
analytics: AnalyticsService,
notificationManager: NotificationManagerProtocol,
@@ -81,6 +83,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
self.windowManager = windowManager
self.bugReportService = bugReportService
self.roomTimelineControllerFactory = roomTimelineControllerFactory
self.appMediator = appMediator
self.appSettings = appSettings
self.analytics = analytics
self.notificationManager = notificationManager
@@ -439,6 +442,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
roomTimelineControllerFactory: roomTimelineControllerFactory,
navigationStackCoordinator: detailNavigationStackCoordinator,
emojiProvider: EmojiProvider(),
appMediator: appMediator,
appSettings: appSettings,
analytics: analytics,
userIndicatorController: ServiceLocator.shared.userIndicatorController,
@@ -16,21 +16,21 @@
import UIKit
extension ApplicationMock {
static var `default`: ApplicationProtocol {
ApplicationMock(withState: .active,
extension AppMediatorMock {
static var `default`: AppMediatorProtocol {
AppMediatorMock(withState: .active,
backgroundTimeRemaining: 10,
allowTasks: true)
}
static var mockBroken: ApplicationProtocol {
ApplicationMock(withState: .inactive,
static var mockBroken: AppMediatorProtocol {
AppMediatorMock(withState: .inactive,
backgroundTimeRemaining: 0,
allowTasks: false)
}
static var mockAboutToSuspend: ApplicationProtocol {
ApplicationMock(withState: .background,
static var mockAboutToSuspend: AppMediatorProtocol {
AppMediatorMock(withState: .background,
backgroundTimeRemaining: 2,
allowTasks: false)
}
@@ -42,7 +42,7 @@ extension ApplicationMock {
allowTasks: Bool) {
self.init()
underlyingApplicationState = applicationState
underlyingAppState = applicationState
underlyingBackgroundTimeRemaining = backgroundTimeRemaining
beginBackgroundTaskWithNameExpirationHandlerClosure = { _, handler in
@@ -747,17 +747,17 @@ class AppLockServiceMock: AppLockServiceProtocol {
}
}
}
class ApplicationMock: ApplicationProtocol {
class AppMediatorMock: AppMediatorProtocol {
var backgroundTimeRemaining: TimeInterval {
get { return underlyingBackgroundTimeRemaining }
set(value) { underlyingBackgroundTimeRemaining = value }
}
var underlyingBackgroundTimeRemaining: TimeInterval!
var applicationState: UIApplication.State {
get { return underlyingApplicationState }
set(value) { underlyingApplicationState = value }
var appState: UIApplication.State {
get { return underlyingAppState }
set(value) { underlyingAppState = value }
}
var underlyingApplicationState: UIApplication.State!
var underlyingAppState: UIApplication.State!
//MARK: - beginBackgroundTask
@@ -19,6 +19,7 @@ import SwiftUI
struct StaticLocationScreenCoordinatorParameters {
let interactionMode: StaticLocationInteractionMode
let appMediator: AppMediatorProtocol
}
enum StaticLocationScreenCoordinatorAction {
@@ -27,7 +28,8 @@ enum StaticLocationScreenCoordinatorAction {
}
final class StaticLocationScreenCoordinator: CoordinatorProtocol {
let viewModel: StaticLocationScreenViewModelProtocol
private let parameters: StaticLocationScreenCoordinatorParameters
private let viewModel: StaticLocationScreenViewModelProtocol
private let actionsSubject: PassthroughSubject<StaticLocationScreenCoordinatorAction, Never> = .init()
private var cancellables = Set<AnyCancellable>()
@@ -37,6 +39,8 @@ final class StaticLocationScreenCoordinator: CoordinatorProtocol {
}
init(parameters: StaticLocationScreenCoordinatorParameters) {
self.parameters = parameters
viewModel = StaticLocationScreenViewModel(interactionMode: parameters.interactionMode)
}
@@ -49,7 +53,7 @@ final class StaticLocationScreenCoordinator: CoordinatorProtocol {
case .close:
actionsSubject.send(.close)
case .openSystemSettings:
UIApplication.shared.openAppSettings()
parameters.appMediator.openAppSettings()
case .sendLocation(let geoURI, let isUserLocation):
actionsSubject.send(.selectedLocation(geoURI, isUserLocation: isUserLocation))
}
@@ -22,6 +22,7 @@ import SwiftUI
struct QRCodeLoginScreenCoordinatorParameters {
let qrCodeLoginService: QRCodeLoginServiceProtocol
let orientationManager: OrientationManagerProtocol
let appMediator: AppMediatorProtocol
}
enum QRCodeLoginScreenCoordinatorAction {
@@ -41,7 +42,7 @@ final class QRCodeLoginScreenCoordinator: CoordinatorProtocol {
init(parameters: QRCodeLoginScreenCoordinatorParameters) {
viewModel = QRCodeLoginScreenViewModel(qrCodeLoginService: parameters.qrCodeLoginService,
application: UIApplication.shared)
appMediator: parameters.appMediator)
orientationManager = parameters.orientationManager
}
@@ -22,7 +22,7 @@ typealias QRCodeLoginScreenViewModelType = StateStoreViewModel<QRCodeLoginScreen
class QRCodeLoginScreenViewModel: QRCodeLoginScreenViewModelType, QRCodeLoginScreenViewModelProtocol {
private let qrCodeLoginService: QRCodeLoginServiceProtocol
private let application: ApplicationProtocol
private let appMediator: AppMediatorProtocol
private let actionsSubject: PassthroughSubject<QRCodeLoginScreenViewModelAction, Never> = .init()
var actionsPublisher: AnyPublisher<QRCodeLoginScreenViewModelAction, Never> {
@@ -30,9 +30,9 @@ class QRCodeLoginScreenViewModel: QRCodeLoginScreenViewModelType, QRCodeLoginScr
}
init(qrCodeLoginService: QRCodeLoginServiceProtocol,
application: ApplicationProtocol) {
appMediator: AppMediatorProtocol) {
self.qrCodeLoginService = qrCodeLoginService
self.application = application
self.appMediator = appMediator
super.init(initialViewState: QRCodeLoginScreenViewState())
}
@@ -45,7 +45,7 @@ class QRCodeLoginScreenViewModel: QRCodeLoginScreenViewModelType, QRCodeLoginScr
case .startScan:
Task { await startScanIfPossible() }
case .openSettings:
application.openAppSettings()
appMediator.openAppSettings()
}
}
@@ -56,7 +56,7 @@ class QRCodeLoginScreenViewModel: QRCodeLoginScreenViewModelType, QRCodeLoginScr
/// Only for mocking initial states
fileprivate init(state: QRCodeLoginState) {
qrCodeLoginService = QRCodeLoginServiceMock(configuration: .init())
application = ApplicationMock()
appMediator = AppMediatorMock.default
super.init(initialViewState: .init(state: state))
}
}
@@ -27,6 +27,7 @@ struct RoomScreenCoordinatorParameters {
let voiceMessageMediaManager: VoiceMessageMediaManagerProtocol
let emojiProvider: EmojiProviderProtocol
let completionSuggestionService: CompletionSuggestionServiceProtocol
let appMediator: AppMediatorProtocol
let appSettings: AppSettings
}
@@ -63,7 +64,7 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
mediaPlayerProvider: parameters.mediaPlayerProvider,
voiceMessageMediaManager: parameters.voiceMessageMediaManager,
userIndicatorController: ServiceLocator.shared.userIndicatorController,
application: UIApplication.shared,
appMediator: parameters.appMediator,
appSettings: parameters.appSettings,
analyticsService: ServiceLocator.shared.analytics,
notificationCenter: NotificationCenter.default)
@@ -40,7 +40,7 @@ class RoomScreenInteractionHandler {
private let voiceMessageRecorder: VoiceMessageRecorderProtocol
private let voiceMessageMediaManager: VoiceMessageMediaManagerProtocol
private let userIndicatorController: UserIndicatorControllerProtocol
private let application: ApplicationProtocol
private let appMediator: AppMediatorProtocol
private let appSettings: AppSettings
private let analyticsService: AnalyticsService
private let pollInteractionHandler: PollInteractionHandlerProtocol
@@ -62,7 +62,7 @@ class RoomScreenInteractionHandler {
voiceMessageMediaManager: VoiceMessageMediaManagerProtocol,
voiceMessageRecorder: VoiceMessageRecorderProtocol,
userIndicatorController: UserIndicatorControllerProtocol,
application: ApplicationProtocol,
appMediator: AppMediatorProtocol,
appSettings: AppSettings,
analyticsService: AnalyticsService) {
self.roomProxy = roomProxy
@@ -72,7 +72,7 @@ class RoomScreenInteractionHandler {
self.voiceMessageMediaManager = voiceMessageMediaManager
self.voiceMessageRecorder = voiceMessageRecorder
self.userIndicatorController = userIndicatorController
self.application = application
self.appMediator = appMediator
self.appSettings = appSettings
self.analyticsService = analyticsService
pollInteractionHandler = PollInteractionHandler(analyticsService: analyticsService, roomProxy: roomProxy)
@@ -623,7 +623,7 @@ class RoomScreenInteractionHandler {
}
private func openSystemSettings() {
application.openAppSettings()
appMediator.openAppSettings()
}
private func displayMediaActionIfPossible(timelineItem: RoomTimelineItemProtocol) async -> RoomTimelineControllerAction {
@@ -32,7 +32,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
private let timelineController: RoomTimelineControllerProtocol
private let mediaPlayerProvider: MediaPlayerProviderProtocol
private let userIndicatorController: UserIndicatorControllerProtocol
private let application: ApplicationProtocol
private let appMediator: AppMediatorProtocol
private let appSettings: AppSettings
private let analyticsService: AnalyticsService
private let notificationCenter: NotificationCenterProtocol
@@ -54,7 +54,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
mediaPlayerProvider: MediaPlayerProviderProtocol,
voiceMessageMediaManager: VoiceMessageMediaManagerProtocol,
userIndicatorController: UserIndicatorControllerProtocol,
application: ApplicationProtocol,
appMediator: AppMediatorProtocol,
appSettings: AppSettings,
analyticsService: AnalyticsService,
notificationCenter: NotificationCenterProtocol) {
@@ -64,7 +64,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
self.appSettings = appSettings
self.analyticsService = analyticsService
self.userIndicatorController = userIndicatorController
self.application = application
self.appMediator = appMediator
self.notificationCenter = notificationCenter
let voiceMessageRecorder = VoiceMessageRecorder(audioRecorder: AudioRecorder(), mediaPlayerProvider: mediaPlayerProvider)
@@ -76,7 +76,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
voiceMessageMediaManager: voiceMessageMediaManager,
voiceMessageRecorder: voiceMessageRecorder,
userIndicatorController: userIndicatorController,
application: application,
appMediator: appMediator,
appSettings: appSettings,
analyticsService: analyticsService)
@@ -417,7 +417,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
}
private func sendReadReceiptIfNeeded(for lastVisibleItemID: TimelineItemIdentifier) async {
guard application.applicationState == .active else { return }
guard appMediator.appState == .active else { return }
await timelineController.sendReadReceipt(for: lastVisibleItemID)
}
@@ -659,7 +659,7 @@ extension RoomScreenViewModel {
mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
userIndicatorController: ServiceLocator.shared.userIndicatorController,
application: ApplicationMock.default,
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
notificationCenter: NotificationCenterMock())
@@ -58,7 +58,7 @@ struct ReadReceiptsSummaryView_Previews: PreviewProvider, TestablePreview {
mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
userIndicatorController: UserIndicatorControllerMock(),
application: ApplicationMock(),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
notificationCenter: NotificationCenterMock())
@@ -192,7 +192,7 @@ struct RoomScreen_Previews: PreviewProvider, TestablePreview {
mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
userIndicatorController: ServiceLocator.shared.userIndicatorController,
application: ApplicationMock.default,
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
notificationCenter: NotificationCenterMock())
@@ -96,7 +96,7 @@ struct TimelineReadReceiptsView_Previews: PreviewProvider, TestablePreview {
mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
userIndicatorController: ServiceLocator.shared.userIndicatorController,
application: ApplicationMock.default,
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
notificationCenter: NotificationCenterMock())
@@ -83,7 +83,7 @@ struct UITimelineView_Previews: PreviewProvider, TestablePreview {
mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
userIndicatorController: ServiceLocator.shared.userIndicatorController,
application: ApplicationMock.default,
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics,
notificationCenter: NotificationCenterMock())
@@ -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"
}
}
}
@@ -126,6 +126,7 @@ class MockScreen: Identifiable {
let flowCoordinator = AuthenticationFlowCoordinator(authenticationService: MockAuthenticationServiceProxy(),
bugReportService: BugReportServiceMock(),
navigationRootCoordinator: navigationRootCoordinator,
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings,
analytics: ServiceLocator.shared.analytics,
userIndicatorController: ServiceLocator.shared.userIndicatorController,
@@ -243,6 +244,7 @@ class MockScreen: Identifiable {
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
emojiProvider: EmojiProvider(),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings)
let coordinator = RoomScreenCoordinator(parameters: parameters)
navigationStackCoordinator.setRootCoordinator(coordinator)
@@ -258,6 +260,7 @@ class MockScreen: Identifiable {
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
emojiProvider: EmojiProvider(),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings)
let coordinator = RoomScreenCoordinator(parameters: parameters)
navigationStackCoordinator.setRootCoordinator(coordinator)
@@ -273,6 +276,7 @@ class MockScreen: Identifiable {
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
emojiProvider: EmojiProvider(),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings)
let coordinator = RoomScreenCoordinator(parameters: parameters)
navigationStackCoordinator.setRootCoordinator(coordinator)
@@ -288,6 +292,7 @@ class MockScreen: Identifiable {
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
emojiProvider: EmojiProvider(),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings)
let coordinator = RoomScreenCoordinator(parameters: parameters)
navigationStackCoordinator.setRootCoordinator(coordinator)
@@ -306,6 +311,7 @@ class MockScreen: Identifiable {
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
emojiProvider: EmojiProvider(),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings)
let coordinator = RoomScreenCoordinator(parameters: parameters)
@@ -324,6 +330,7 @@ class MockScreen: Identifiable {
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
emojiProvider: EmojiProvider(),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings)
let coordinator = RoomScreenCoordinator(parameters: parameters)
@@ -342,6 +349,7 @@ class MockScreen: Identifiable {
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
emojiProvider: EmojiProvider(),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings)
let coordinator = RoomScreenCoordinator(parameters: parameters)
@@ -361,6 +369,7 @@ class MockScreen: Identifiable {
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
emojiProvider: EmojiProvider(),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings)
let coordinator = RoomScreenCoordinator(parameters: parameters)
@@ -379,6 +388,7 @@ class MockScreen: Identifiable {
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
emojiProvider: EmojiProvider(),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings)
let coordinator = RoomScreenCoordinator(parameters: parameters)
@@ -397,6 +407,7 @@ class MockScreen: Identifiable {
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
emojiProvider: EmojiProvider(),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings)
let coordinator = RoomScreenCoordinator(parameters: parameters)
@@ -415,6 +426,7 @@ class MockScreen: Identifiable {
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
emojiProvider: EmojiProvider(),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings)
let coordinator = RoomScreenCoordinator(parameters: parameters)
@@ -433,6 +445,7 @@ class MockScreen: Identifiable {
voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
emojiProvider: EmojiProvider(),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings)
let coordinator = RoomScreenCoordinator(parameters: parameters)
@@ -460,6 +473,7 @@ class MockScreen: Identifiable {
appSettings: ServiceLocator.shared.settings),
bugReportService: BugReportServiceMock(),
roomTimelineControllerFactory: MockRoomTimelineControllerFactory(),
appMediator: AppMediatorMock.default,
appSettings: appSettings,
analytics: ServiceLocator.shared.analytics,
notificationManager: NotificationManagerMock(),