1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-20 00:38:24 +02:00
immich/mobile/ios/Runner/BackgroundSync/BackgroundSyncWorker.swift

189 lines
7.4 KiB
Swift
Raw Normal View History

feat(mobile): iOS background sync (#1758) * first run of getting background sync working in iOS * got background sync calling into flutter * added background task * added necessary sync files * fixed some names and added more implementations * got as far as Hive.initFlutter * brute force got to await Hive.initFlutter * lots of print statements to figure out where execution is failing, and its failing at the root asset bundle in the localization.dart service * first time working, got plugins registered * removed broken cleanup code * refactored * linters * now can pass user settings * background service plugin uses app background processing instead of fetch * renamed backgroundFetch to backgroundProcessing to make it clearer * don't use max delay * adds fetch back in * fixes require charging default values and backup controller page * fixes background fetch * fixes ios not importing photos * guarded path provider ios * lint * adds max tries for heartbeat to work in iOS * fail after seconds * timeout instead of fail after seconds * removes release lock from system stop * restores checkLockReleasedWithHeartbeat to Future<void> * removes max tries from acquire lock * fixes lock timeout with iOS * restored for loop * adds comments, made the AppRefresh task only run while not requiring network or charge * fixed compile issue * now both are registered and added better comments. also added ability for task to cancel itself * added the podfile and pubspec * added backup diagnostics to IOS and removed iOS ignored backup options and fixed network connectivity always required * Added Alex's dev team * styled debug list item, fixed refresh task not set bug, fixed enable / disable background service on platform channel --------- Co-authored-by: Marty Fuhry <marty@fuhry.farm> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-02-20 07:59:50 +02:00
//
// BackgroundSyncProcessing.swift
// Runner
//
// Created by Marty Fuhry on 2/6/23.
//
// Credit to https://github.com/fluttercommunity/flutter_workmanager/blob/main/ios/Classes/BackgroundWorker.swift
import Foundation
import Flutter
import BackgroundTasks
// The background worker which creates a new Flutter VM, communicates with it
// to run the backup job, and then finishes execution and calls back to its callback
// handler
class BackgroundSyncWorker {
// The Flutter engine we create for background execution.
// This is not the main Flutter engine which shows the UI,
// this is a brand new isolate created and managed in this code
// here. It does not share memory with the main
// Flutter engine which shows the UI.
// It needs to be started up, registered, and torn down here
let engine: FlutterEngine? = FlutterEngine(
name: "BackgroundImmich"
)
// The background message passing channel
var channel: FlutterMethodChannel?
var completionHandler: (UIBackgroundFetchResult) -> Void
let taskSessionStart = Date()
// We need the completion handler to tell the system when we are done running
init(_ completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
// This is the background message passing channel to be used with the background engine
// created here in this platform code
self.channel = FlutterMethodChannel(
name: "immich/backgroundChannel",
binaryMessenger: engine!.binaryMessenger
)
self.completionHandler = completionHandler
}
// Handles all of the messages from the Flutter VM called into this platform code
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "initialized":
// Initialize tells us that we can now call into the Flutter VM to tell it to begin the update
self.channel?.invokeMethod(
"backgroundProcessing",
arguments: nil,
result: { flutterResult in
// This is the result we send back to the BGTaskScheduler to let it know whether we'll need more time later or
// if this execution failed
let result: UIBackgroundFetchResult = (flutterResult as? Bool ?? false) ? .newData : .failed
// Show the task duration
let taskSessionCompleter = Date()
let taskDuration = taskSessionCompleter.timeIntervalSince(self.taskSessionStart)
print("[\(String(describing: self))] \(#function) -> performBackgroundRequest.\(result) (finished in \(taskDuration) seconds)")
// Complete the execution
self.complete(result)
})
break
case "updateNotification":
// TODO: implement update notification
result(true)
break
case "showError":
// TODO: implement show error
result(true)
break
case "clearErrorNotifications":
// TODO: implement clear error notifications
result(true)
break
case "hasContentChanged":
// This is only called for Android, but we provide an implementation here
// telling Flutter that we don't have any information about whether the gallery
// contents have changed or not, so we can just say "no, they've not changed"
result(false)
break
default:
result(FlutterError())
self.complete(UIBackgroundFetchResult.failed)
}
}
// Runs the background sync by starting up a new isolate and handling the calls
// until it completes
public func run(maxSeconds: Int?) {
// We need the callback handle to start up the Flutter VM from the entry point
let defaults = UserDefaults.standard
guard let callbackHandle = defaults.value(forKey: "callback_handle") as? Int64 else {
// Can't find the callback handle, this is fatal
complete(UIBackgroundFetchResult.failed)
return
}
// Use the provided callbackHandle to get the callback function
guard let callback = FlutterCallbackCache.lookupCallbackInformation(callbackHandle) else {
// We need this callback or else this is fatal
complete(UIBackgroundFetchResult.failed)
return
}
// Sanity check for the engine existing
if engine == nil {
complete(UIBackgroundFetchResult.failed)
return
}
// Run the engine
let isRunning = engine!.run(
withEntrypoint: callback.callbackName,
libraryURI: callback.callbackLibraryPath
)
// If this engine isn't running, this is fatal
if !isRunning {
complete(UIBackgroundFetchResult.failed)
return
}
// If we have a timer, we need to start the timer to cancel ourselves
// so that we don't run longer than the provided maxSeconds
// After maxSeconds has elapsed, we will invoke "systemStop"
if maxSeconds != nil {
// Schedule a non-repeating timer to run after maxSeconds
let timer = Timer.scheduledTimer(withTimeInterval: TimeInterval(maxSeconds!),
repeats: false) { timer in
// The callback invalidates the timer and stops execution
timer.invalidate()
// If the channel is already deallocated, we don't need to do anything
if self.channel == nil {
return
}
// Tell the Flutter VM to stop backing up now
self.channel?.invokeMethod(
"systemStop",
arguments: nil,
result: nil)
// Complete the execution
self.complete(UIBackgroundFetchResult.newData)
}
}
// Set the handle function to the channel message handler
self.channel?.setMethodCallHandler(handle)
// Register this to get access to the plugins on the platform channel
BackgroundServicePlugin.flutterPluginRegistrantCallback?(engine!)
}
// Cancels execution of this task, used by the system's task expiration handler
// which is called shortly before execution is about to expire
public func cancel() {
// If the channel is already deallocated, we don't need to do anything
if self.channel == nil {
return
}
// Tell the Flutter VM to stop backing up now
self.channel?.invokeMethod(
"systemStop",
arguments: nil,
result: nil)
// Complete the execution
self.complete(UIBackgroundFetchResult.newData)
}
// Completes the execution, destroys the engine, and sends a completion to our callback completionHandler
private func complete(_ fetchResult: UIBackgroundFetchResult) {
engine?.destroyContext()
channel = nil
completionHandler(fetchResult)
}
}