mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 18:58:56 -04:00 
			
		
		
		
	* adds notification handling logic * notification on background updates for iOS * fixed regression where i accidentally removed load translations from the background sync * fixed ios translations --------- Co-authored-by: Marty Fuhry <marty@fuhry.farm> Co-authored-by: Alex <alex.tran1502@gmail.com>
		
			
				
	
	
		
			272 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Swift
		
	
	
	
	
	
			
		
		
	
	
			272 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Swift
		
	
	
	
	
	
| //
 | |
| //  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"
 | |
|     )
 | |
|     
 | |
|     let notificationId = "com.alextran.immich/backgroundNotifications"
 | |
|     // 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":
 | |
|             let handled = self.handleNotification(call)
 | |
|             result(handled)
 | |
|             break
 | |
|         case "showError":
 | |
|             let handled = self.handleError(call)
 | |
|             result(handled)
 | |
|             break
 | |
|         case "clearErrorNotifications":
 | |
|             self.handleClearErrorNotifications()
 | |
|             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)
 | |
|     }
 | |
|     
 | |
|     private func handleNotification(_ call: FlutterMethodCall) -> Bool {
 | |
|         
 | |
|         // Parse the arguments as an array list
 | |
|         guard let args = call.arguments as? Array<Any> else {
 | |
|             print("Failed to parse \(call.arguments) as array")
 | |
|             return false;
 | |
|         }
 | |
|         
 | |
|         // Requires 7 arguments passed or else fail
 | |
|         guard args.count == 7 else {
 | |
|             print("Needs 7 arguments, but was only passed \(args.count)")
 | |
|             return false
 | |
|         }
 | |
|         
 | |
|         // Parse the arguments to send the notification update
 | |
|         let title = args[0] as? String
 | |
|         let content = args[1] as? String
 | |
|         let progress = args[2] as? Int
 | |
|         let maximum = args[3] as? Int
 | |
|         let indeterminate = args[4] as? Bool
 | |
|         let isDetail = args[5] as? Bool
 | |
|         let onlyIfForeground = args[6] as? Bool
 | |
|         
 | |
|         // Build the notification
 | |
|         let notificationContent = UNMutableNotificationContent()
 | |
|         notificationContent.body = content ?? "Uploading..."
 | |
|         notificationContent.title = title ?? "Immich"
 | |
|         
 | |
|         // Add it to the Notification center
 | |
|         let notification = UNNotificationRequest(
 | |
|             identifier: notificationId,
 | |
|             content: notificationContent,
 | |
|             trigger: nil
 | |
|         )
 | |
|         let center = UNUserNotificationCenter.current()
 | |
|         center.add(notification) { (error: Error?) in
 | |
|             if let theError = error {
 | |
|                 print("Error showing notifications: \(theError)")
 | |
|             }
 | |
|         }
 | |
|         
 | |
|         return true
 | |
|     }
 | |
|     
 | |
|     private func handleError(_ call: FlutterMethodCall) -> Bool {
 | |
|         // Parse the arguments as an array list
 | |
|         guard let args = call.arguments as? Array<Any> else {
 | |
|             return false;
 | |
|         }
 | |
|         
 | |
|         // Requires 7 arguments passed or else fail
 | |
|         guard args.count == 3 else {
 | |
|             return false
 | |
|         }
 | |
|         
 | |
|         let title = args[0] as? String
 | |
|         let content = args[1] as? String
 | |
|         let individualTag = args[2] as? String
 | |
|         
 | |
|         // Build the notification
 | |
|         let notificationContent = UNMutableNotificationContent()
 | |
|         notificationContent.body = content ?? "Error running the backup job."
 | |
|         notificationContent.title = title ?? "Immich"
 | |
|         
 | |
|         // Add it to the Notification center
 | |
|         let notification = UNNotificationRequest(
 | |
|             identifier: notificationId,
 | |
|             content: notificationContent,
 | |
|             trigger: nil
 | |
|         )
 | |
|         let center = UNUserNotificationCenter.current()
 | |
|         center.add(notification)
 | |
|         
 | |
|         return true
 | |
|     }
 | |
|     
 | |
|     private func handleClearErrorNotifications() {
 | |
|         let center = UNUserNotificationCenter.current()
 | |
|         center.removeDeliveredNotifications(withIdentifiers: [notificationId])
 | |
|         center.removePendingNotificationRequests(withIdentifiers: [notificationId])
 | |
|     }
 | |
| }
 | |
| 
 |