Skip to content

Commit

Permalink
Add background app refresh support
Browse files Browse the repository at this point in the history
  • Loading branch information
stefanceriu committed May 15, 2023
1 parent 1ca7172 commit 32ebe7c
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 9 deletions.
87 changes: 78 additions & 9 deletions ElementX/Sources/Application/AppCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
// limitations under the License.
//

import BackgroundTasks
import Combine
import MatrixRustSDK
import SwiftUI
Expand Down Expand Up @@ -49,6 +50,7 @@ class AppCoordinator: AppCoordinatorProtocol {
private var userSessionObserver: AnyCancellable?
private var networkMonitorObserver: AnyCancellable?
private var initialSyncObserver: AnyCancellable?
private var backgroundRefreshSyncObserver: AnyCancellable?

let notificationManager: NotificationManagerProtocol

Expand Down Expand Up @@ -93,6 +95,8 @@ class AppCoordinator: AppCoordinatorProtocol {

observeApplicationState()
observeNetworkState()

registerBackgroundAppRefresh()
}

func start() {
Expand Down Expand Up @@ -143,6 +147,7 @@ class AppCoordinator: AppCoordinatorProtocol {
userSessionStore.reset()
}

// swiftlint:disable:next cyclomatic_complexity
private func setupStateMachine() {
stateMachine.addTransitionHandler { [weak self] context in
guard let self else { return }
Expand Down Expand Up @@ -342,7 +347,7 @@ class AppCoordinator: AppCoordinatorProtocol {
case .didReceiveAuthError(let isSoftLogout):
stateMachine.processEvent(.signOut(isSoft: isSoftLogout))
case .updateRestorationToken:
userSessionStore.refreshRestorationToken(for: userSession)
_ = userSessionStore.refreshRestorationToken(for: userSession)
default:
break
}
Expand Down Expand Up @@ -384,11 +389,10 @@ class AppCoordinator: AppCoordinatorProtocol {
initialSyncObserver = userSession.clientProxy
.callbacks
.receive(on: DispatchQueue.main)
.sink { [weak self] callback in
if case .receivedSyncUpdate = callback {
ServiceLocator.shared.userIndicatorController.retractIndicatorWithId(identifier)
self?.initialSyncObserver?.cancel()
}
.filter(\.isSyncUpdate)
.sink { [weak self] _ in
ServiceLocator.shared.userIndicatorController.retractIndicatorWithId(identifier)
self?.initialSyncObserver?.cancel()
}
}

Expand All @@ -412,11 +416,17 @@ class AppCoordinator: AppCoordinatorProtocol {
}

backgroundTask = backgroundTaskService.startBackgroundTask(withName: "SuspendApp: \(UUID().uuidString)") { [weak self] in
self?.stopSync()
guard let self else { return }

self?.backgroundTask = nil
self?.isSuspended = true
stopSync()

backgroundTask = nil
isSuspended = true
}

// This does seem to work if scheduled from the background task above
// Schedule it here instead but with an earliest being date of 30 seconds
scheduleBackgroundAppRefresh()
}

@objc
Expand All @@ -432,6 +442,8 @@ class AppCoordinator: AppCoordinatorProtocol {
}
}

// MARK: Other

private func observeNetworkState() {
let reachabilityNotificationIdentifier = "io.element.elementx.reachability.notification"
networkMonitorObserver = ServiceLocator.shared.networkMonitor.reachabilityPublisher.sink { reachable in
Expand Down Expand Up @@ -474,6 +486,63 @@ class AppCoordinator: AppCoordinatorProtocol {
hideLoadingIndicator()
}
}

// MARK: Background app refresh

private func registerBackgroundAppRefresh() {
let result = BGTaskScheduler.shared.register(forTaskWithIdentifier: ServiceLocator.shared.settings.backgroundAppRefreshTaskIdentifier, using: .main) { [weak self] task in
guard let task = task as? BGAppRefreshTask else {
MXLog.error("Invalid background app refresh configuration")
return
}

self?.handleBackgroundAppRefresh(task)
}

MXLog.info("Register background app refresh with result: \(result)")
}

private func scheduleBackgroundAppRefresh() {
let request = BGAppRefreshTaskRequest(identifier: ServiceLocator.shared.settings.backgroundAppRefreshTaskIdentifier)

// We have other background tasks that keep the app alive
request.earliestBeginDate = Date(timeIntervalSinceNow: 30)

do {
try BGTaskScheduler.shared.submit(request)
MXLog.info("Successfully scheduled background app refresh task")
} catch {
MXLog.error("Failed scheduling background app refresh with error :\(error)")
}
}

private func handleBackgroundAppRefresh(_ task: BGAppRefreshTask) {
MXLog.info("Started background app refresh")

// This is important for the app to keep refreshing in the background
scheduleBackgroundAppRefresh()

task.expirationHandler = { [weak self] in
MXLog.info("Background app refresh task expired")
self?.stopSync()
task.setTaskCompleted(success: true)
}

startSync()

// Be a good citizen, run for a max of 10 SS responses or 10 seconds
// An SS request will time out after 30 seconds if no new data is available
backgroundRefreshSyncObserver = userSession?.clientProxy
.callbacks
.filter(\.isSyncUpdate)
.collect(.byTimeOrCount(DispatchQueue.main, .seconds(10), 10))
.sink(receiveValue: { [weak self] _ in
MXLog.info("Background app refresh finished")
self?.backgroundRefreshSyncObserver?.cancel()
self?.stopSync()
task.setTaskCompleted(success: true)
})
}
}

// MARK: - AuthenticationCoordinatorDelegate
Expand Down
3 changes: 3 additions & 0 deletions ElementX/Sources/Application/AppSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ final class AppSettings {
/// that don't yet have an officially trusted proxy configured in their well-known.
let slidingSyncProxyURL: URL? = nil

/// The task identifier used for background app refresh. Also used in main target's the Info.plist
let backgroundAppRefreshTaskIdentifier = "io.element.elementx.background.refresh"

// MARK: - Authentication

/// The URL that is opened when tapping the Learn more button on the sliding sync alert during authentication.
Expand Down
8 changes: 8 additions & 0 deletions ElementX/Sources/Services/Client/ClientProxyProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ enum ClientProxyCallback {
case receivedAuthError(isSoftLogout: Bool)
case receivedNotification(NotificationItemProxyProtocol)
case updateRestorationToken

var isSyncUpdate: Bool {
if case .receivedSyncUpdate = self {
return true
} else {
return false
}
}
}

enum ClientProxyError: Error {
Expand Down
8 changes: 8 additions & 0 deletions ElementX/SupportingFiles/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>io.element.elementx.background.refresh</string>
</array>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
Expand Down Expand Up @@ -30,6 +34,10 @@
<array>
<string>INSendMessageIntent</string>
</array>
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UISupportedInterfaceOrientations</key>
Expand Down
6 changes: 6 additions & 0 deletions ElementX/SupportingFiles/target.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ targets:
]
NSCameraUsageDescription: The camera is used to take and upload photos and videos.
NSMicrophoneUsageDescription: The microphone is used to take videos.
UIBackgroundModes: [
fetch
]
BGTaskSchedulerPermittedIdentifiers: [
io.element.elementx.background.refresh
]


settings:
Expand Down

0 comments on commit 32ebe7c

Please sign in to comment.