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) { 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) } }