mirror of
https://github.com/immich-app/immich.git
synced 2025-08-30 23:02:39 -04:00
* feat: ios background sync # Conflicts: # mobile/ios/Runner/Info.plist * feat: Android sync * add local sync worker and rename stuff * group upload notifications * uncomment onresume beta handling * rename methods --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Alex <alex.tran1502@gmail.com>
203 lines
8.5 KiB
Swift
203 lines
8.5 KiB
Swift
import BackgroundTasks
|
|
import Flutter
|
|
|
|
enum BackgroundTaskType { case localSync, refreshUpload, processingUpload }
|
|
|
|
/*
|
|
* DEBUG: Testing Background Tasks in Xcode
|
|
*
|
|
* To test background task functionality during development:
|
|
* 1. Pause the application in Xcode debugger
|
|
* 2. In the debugger console, enter one of the following commands:
|
|
|
|
## For local sync (short-running sync):
|
|
|
|
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"app.alextran.immich.background.localSync"]
|
|
|
|
## For background refresh (short-running sync):
|
|
|
|
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"app.alextran.immich.background.refreshUpload"]
|
|
|
|
## For background processing (long-running upload):
|
|
|
|
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"app.alextran.immich.background.processingUpload"]
|
|
|
|
* To simulate task expiration (useful for testing expiration handlers):
|
|
|
|
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"app.alextran.immich.background.localSync"]
|
|
|
|
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"app.alextran.immich.background.refreshUpload"]
|
|
|
|
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"app.alextran.immich.background.processingUpload"]
|
|
|
|
* 3. Resume the application to see the background code execute
|
|
*
|
|
* NOTE: This must be tested on a physical device, not in the simulator.
|
|
* In testing, only the background processing task can be reliably simulated.
|
|
* These commands submit the respective task to BGTaskScheduler for immediate processing.
|
|
* Use the expiration commands to test how the app handles iOS terminating background tasks.
|
|
*/
|
|
|
|
|
|
/// 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.
|
|
/// This class manages a separate Flutter engine instance for background execution,
|
|
/// independent of the main UI Flutter engine.
|
|
class BackgroundWorker: BackgroundWorkerBgHostApi {
|
|
private let taskType: BackgroundTaskType
|
|
/// The maximum number of seconds to run the task before timing out
|
|
private let maxSeconds: Int?
|
|
/// Callback function to invoke when the background task completes
|
|
private let completionHandler: (_ success: Bool) -> Void
|
|
|
|
/// The Flutter engine created specifically for background execution.
|
|
/// This is a separate instance from the main Flutter engine that handles the UI.
|
|
/// It operates in its own isolate and doesn't share memory with the main engine.
|
|
/// Must be properly started, registered, and torn down during background execution.
|
|
private let engine = FlutterEngine(name: "BackgroundImmich")
|
|
|
|
/// Used to call methods on the flutter side
|
|
private var flutterApi: BackgroundWorkerFlutterApi?
|
|
|
|
/// Flag to track whether the background task has completed to prevent duplicate completions
|
|
private var isComplete = false
|
|
|
|
/**
|
|
* Initializes a new background worker with the specified task type and execution constraints.
|
|
* Creates a new Flutter engine instance for background execution and sets up the necessary
|
|
* communication channels between native iOS and Flutter code.
|
|
*
|
|
* - Parameters:
|
|
* - taskType: The type of background task to execute (upload or sync task)
|
|
* - maxSeconds: Optional maximum execution time in seconds before the task is cancelled
|
|
* - completionHandler: Callback function invoked when the task completes, with success status
|
|
*/
|
|
init(taskType: BackgroundTaskType, maxSeconds: Int?, completionHandler: @escaping (_ success: Bool) -> Void) {
|
|
self.taskType = taskType
|
|
self.maxSeconds = maxSeconds
|
|
self.completionHandler = completionHandler
|
|
// Should be initialized only after the engine starts running
|
|
self.flutterApi = nil
|
|
}
|
|
|
|
/**
|
|
* Starts the background Flutter engine and begins execution of the background task.
|
|
* Retrieves the callback handle from UserDefaults, looks up the Flutter callback,
|
|
* starts the engine, and sets up a timeout timer if specified.
|
|
*/
|
|
func run() {
|
|
// Retrieve the callback handle stored by the main Flutter app
|
|
// This handle points to the Flutter function that should be executed in the background
|
|
let callbackHandle = Int64(UserDefaults.standard.string(
|
|
forKey: BackgroundWorkerApiImpl.backgroundUploadCallbackHandleKey) ?? "0") ?? 0
|
|
|
|
if callbackHandle == 0 {
|
|
// Without a valid callback handle, we cannot start the Flutter background execution
|
|
complete(success: false)
|
|
return
|
|
}
|
|
|
|
// Use the callback handle to retrieve the actual Flutter callback information
|
|
guard let callback = FlutterCallbackCache.lookupCallbackInformation(callbackHandle) else {
|
|
// The callback handle is invalid or the callback was not found
|
|
complete(success: false)
|
|
return
|
|
}
|
|
|
|
// Start the Flutter engine with the specified callback as the entry point
|
|
let isRunning = engine.run(
|
|
withEntrypoint: callback.callbackName,
|
|
libraryURI: callback.callbackLibraryPath
|
|
)
|
|
|
|
// Verify that the Flutter engine started successfully
|
|
if !isRunning {
|
|
complete(success: false)
|
|
return
|
|
}
|
|
|
|
// Register plugins in the new engine
|
|
GeneratedPluginRegistrant.register(with: engine)
|
|
// Register custom plugins
|
|
AppDelegate.registerPlugins(binaryMessenger: engine.binaryMessenger)
|
|
flutterApi = BackgroundWorkerFlutterApi(binaryMessenger: engine.binaryMessenger)
|
|
BackgroundWorkerBgHostApiSetup.setUp(binaryMessenger: engine.binaryMessenger, api: self)
|
|
|
|
// Set up a timeout timer if maxSeconds was specified to prevent runaway background tasks
|
|
if maxSeconds != nil {
|
|
// Schedule a timer to cancel the task after the specified timeout period
|
|
Timer.scheduledTimer(withTimeInterval: TimeInterval(maxSeconds!), repeats: false) { _ in
|
|
self.cancel()
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called by the Flutter side when it has finished initialization and is ready to receive commands.
|
|
* Routes the appropriate task type (refresh or processing) to the corresponding Flutter method.
|
|
* This method acts as a bridge between the native iOS background task system and Flutter.
|
|
*/
|
|
func onInitialized() throws {
|
|
switch self.taskType {
|
|
case .refreshUpload, .processingUpload:
|
|
flutterApi?.onIosUpload(isRefresh: self.taskType == .refreshUpload,
|
|
maxSeconds: maxSeconds.map { Int64($0) }, completion: { result in
|
|
self.handleHostResult(result: result)
|
|
})
|
|
case .localSync:
|
|
flutterApi?.onLocalSync(maxSeconds: maxSeconds.map { Int64($0) }, completion: { result in
|
|
self.handleHostResult(result: result)
|
|
})
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cancels the currently running background task, either due to timeout or external request.
|
|
* Sends a cancel signal to the Flutter side and sets up a fallback timer to ensure
|
|
* the completion handler is eventually called even if Flutter doesn't respond.
|
|
*/
|
|
func cancel() {
|
|
if isComplete {
|
|
return
|
|
}
|
|
|
|
isComplete = true
|
|
flutterApi?.cancel { result in
|
|
self.complete(success: false)
|
|
}
|
|
|
|
// Fallback safety mechanism: ensure completion is called within 2 seconds
|
|
// This prevents the background task from hanging indefinitely if Flutter doesn't respond
|
|
Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { _ in
|
|
self.complete(success: false)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles the result from Flutter API calls and determines the success/failure status.
|
|
* Converts Flutter's Result type to a simple boolean success indicator for task completion.
|
|
*
|
|
* - Parameter result: The result returned from a Flutter API call
|
|
*/
|
|
private func handleHostResult(result: Result<Void, PigeonError>) {
|
|
switch result {
|
|
case .success(): self.complete(success: true)
|
|
case .failure(_): self.cancel()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cleans up resources by destroying the Flutter engine context and invokes the completion handler.
|
|
* This method ensures that the background task is marked as complete, releases the Flutter engine,
|
|
* and notifies the caller of the task's success or failure. This is the final step in the
|
|
* background task lifecycle and should only be called once per task instance.
|
|
*
|
|
* - Parameter success: Indicates whether the background task completed successfully
|
|
*/
|
|
private func complete(success: Bool) {
|
|
isComplete = true
|
|
engine.destroyContext()
|
|
completionHandler(success)
|
|
}
|
|
}
|