You've already forked immich
mirror of
https://github.com/immich-app/immich.git
synced 2025-08-08 23:07:06 +02:00
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>
This commit is contained in:
188
mobile/ios/Runner/BackgroundSync/BackgroundSyncWorker.swift
Normal file
188
mobile/ios/Runner/BackgroundSync/BackgroundSyncWorker.swift
Normal file
@ -0,0 +1,188 @@
|
||||
//
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user