mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-03 19:17:11 -05:00 
			
		
		
		
	* adds background app refresh message * fixes ios background settings provider * styling * capitalization * changed to watch * uses settings notifier now * forgot to commit this file * changed to watch and added more clarification --------- Co-authored-by: Marty Fuhry <marty@fuhry.farm>
		
			
				
	
	
		
			343 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Swift
		
	
	
	
	
	
			
		
		
	
	
			343 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Swift
		
	
	
	
	
	
//
 | 
						|
//  BackgroundServicePlugin.swift
 | 
						|
//  Runner
 | 
						|
//
 | 
						|
//  Created by Marty Fuhry on 2/14/23.
 | 
						|
//
 | 
						|
 | 
						|
import Flutter
 | 
						|
import BackgroundTasks
 | 
						|
import path_provider_foundation
 | 
						|
 | 
						|
class BackgroundServicePlugin: NSObject, FlutterPlugin {
 | 
						|
    
 | 
						|
    public static var flutterPluginRegistrantCallback: FlutterPluginRegistrantCallback?
 | 
						|
    
 | 
						|
    public static func setPluginRegistrantCallback(_ callback: FlutterPluginRegistrantCallback) {
 | 
						|
        flutterPluginRegistrantCallback = callback
 | 
						|
    }
 | 
						|
 | 
						|
    //  Pause the application in XCode, then enter
 | 
						|
    // e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"app.alextran.immich.backgroundFetch"]
 | 
						|
    // or
 | 
						|
    // e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"app.alextran.immich.backgroundProcessing"]
 | 
						|
    // Then resume the application see the background code run
 | 
						|
    // Tested on a physical device, not a simulator
 | 
						|
    // This will submit either the Fetch or Processing command to the BGTaskScheduler for immediate processing.
 | 
						|
    // In my tests, I can only get app.alextran.immich.backgroundProcessing simulated by running the above command
 | 
						|
    
 | 
						|
    // This is the task ID in Info.plist to register as our background task ID
 | 
						|
    public static let backgroundFetchTaskID = "app.alextran.immich.backgroundFetch"
 | 
						|
    public static let backgroundProcessingTaskID = "app.alextran.immich.backgroundProcessing"
 | 
						|
    
 | 
						|
    // Establish communication with the main isolate and set up the channel call
 | 
						|
    // to this BackgroundServicePlugion()
 | 
						|
    public static func register(with registrar: FlutterPluginRegistrar) {
 | 
						|
        let channel = FlutterMethodChannel(
 | 
						|
            name: "immich/foregroundChannel",
 | 
						|
            binaryMessenger: registrar.messenger()
 | 
						|
        )
 | 
						|
 | 
						|
        let instance = BackgroundServicePlugin()
 | 
						|
        registrar.addMethodCallDelegate(instance, channel: channel)
 | 
						|
        registrar.addApplicationDelegate(instance)
 | 
						|
    }
 | 
						|
    
 | 
						|
    // Registers the Flutter engine with the plugins, used by the other Background Flutter engine
 | 
						|
    public static func register(engine: FlutterEngine) {
 | 
						|
        GeneratedPluginRegistrant.register(with: engine)
 | 
						|
    }
 | 
						|
 | 
						|
    // Registers the task IDs from the system so that we can process them here in this class
 | 
						|
    public static func registerBackgroundProcessing() {
 | 
						|
 
 | 
						|
        let processingRegisterd = BGTaskScheduler.shared.register(
 | 
						|
            forTaskWithIdentifier: backgroundProcessingTaskID,
 | 
						|
            using: nil) { task in
 | 
						|
            if task is BGProcessingTask {
 | 
						|
                handleBackgroundProcessing(task: task as! BGProcessingTask)
 | 
						|
            }
 | 
						|
        }
 | 
						|
        
 | 
						|
        let fetchRegisterd = BGTaskScheduler.shared.register(
 | 
						|
            forTaskWithIdentifier: backgroundFetchTaskID,
 | 
						|
            using: nil) { task in
 | 
						|
            if task is BGAppRefreshTask {
 | 
						|
                handleBackgroundFetch(task: task as! BGAppRefreshTask)
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    // Handles the channel methods from Flutter
 | 
						|
    public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
 | 
						|
        switch call.method {
 | 
						|
            case "enable":
 | 
						|
                handleBackgroundEnable(call: call, result: result)
 | 
						|
                break
 | 
						|
            case "configure":
 | 
						|
                handleConfigure(call: call, result: result)
 | 
						|
                break
 | 
						|
            case "disable":
 | 
						|
                handleDisable(call: call, result: result)
 | 
						|
                break
 | 
						|
            case "isEnabled":
 | 
						|
                handleIsEnabled(call: call, result: result)
 | 
						|
                break
 | 
						|
            case "isIgnoringBatteryOptimizations":
 | 
						|
                result(FlutterMethodNotImplemented)
 | 
						|
                break
 | 
						|
            case "lastBackgroundFetchTime":
 | 
						|
                let defaults = UserDefaults.standard
 | 
						|
                let lastRunTime = defaults.value(forKey: "last_background_fetch_run_time")
 | 
						|
                result(lastRunTime)
 | 
						|
                break
 | 
						|
            case "lastBackgroundProcessingTime":
 | 
						|
                let defaults = UserDefaults.standard
 | 
						|
                let lastRunTime = defaults.value(forKey: "last_background_processing_run_time")
 | 
						|
                result(lastRunTime)
 | 
						|
                break
 | 
						|
            case "numberOfBackgroundProcesses":
 | 
						|
                handleNumberOfProcesses(call: call, result: result)
 | 
						|
                break
 | 
						|
            case "backgroundAppRefreshEnabled":
 | 
						|
                handleBackgroundRefreshStatus(call: call, result: result)
 | 
						|
                break
 | 
						|
            default:
 | 
						|
                result(FlutterMethodNotImplemented)
 | 
						|
                break
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    // Called by the flutter code when enabled so that we can turn on the backround services
 | 
						|
    // and save the callback information to communicate on this method channel
 | 
						|
    public func handleBackgroundEnable(call: FlutterMethodCall, result: FlutterResult) {
 | 
						|
        
 | 
						|
        // Needs to parse the arguments from the method call
 | 
						|
        guard let args = call.arguments as? Array<Any> else {
 | 
						|
            print("Cannot parse args as array: \(call.arguments)")
 | 
						|
            result(FlutterMethodNotImplemented)
 | 
						|
            return
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Requires 3 arguments in the array
 | 
						|
        guard args.count == 3 else {
 | 
						|
            print("Requires 3 arguments and received \(args.count)")
 | 
						|
            result(FlutterMethodNotImplemented)
 | 
						|
            return
 | 
						|
        }
 | 
						|
                
 | 
						|
        // Parses the arguments
 | 
						|
        let callbackHandle = args[0] as? Int64
 | 
						|
        let notificationTitle = args[1] as? String
 | 
						|
        let instant = args[2] as? Bool
 | 
						|
        
 | 
						|
        // Write enabled to settings
 | 
						|
        let defaults = UserDefaults.standard
 | 
						|
        
 | 
						|
        // We are now enabled, so store this
 | 
						|
        defaults.set(true, forKey: "background_service_enabled")
 | 
						|
        
 | 
						|
        // The callback handle is an int64 address to communicate with the main isolate's
 | 
						|
        // entry function
 | 
						|
        defaults.set(callbackHandle, forKey: "callback_handle")
 | 
						|
        
 | 
						|
        // This is not used yet and will need to be implemented
 | 
						|
        defaults.set(notificationTitle, forKey: "notification_title")
 | 
						|
        
 | 
						|
        // Schedule the background services
 | 
						|
        BackgroundServicePlugin.scheduleBackgroundSync()
 | 
						|
        BackgroundServicePlugin.scheduleBackgroundFetch()
 | 
						|
 | 
						|
        result(true)
 | 
						|
    }
 | 
						|
    
 | 
						|
    // Called by the flutter code at launch to see if the background service is enabled or not
 | 
						|
    func handleIsEnabled(call: FlutterMethodCall, result: FlutterResult) {
 | 
						|
        let defaults = UserDefaults.standard
 | 
						|
        let enabled = defaults.value(forKey: "background_service_enabled") as? Bool
 | 
						|
        
 | 
						|
        // False by default
 | 
						|
        result(enabled ?? false)
 | 
						|
    }
 | 
						|
    
 | 
						|
    // Called by the Flutter code whenever a change in configuration is set
 | 
						|
    func handleConfigure(call: FlutterMethodCall, result: FlutterResult) {
 | 
						|
        
 | 
						|
        // Needs to be able to parse the arguments or else fail
 | 
						|
        guard let args = call.arguments as? Array<Any> else {
 | 
						|
            print("Cannot parse args as array: \(call.arguments)")
 | 
						|
            result(FlutterError())
 | 
						|
            return
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Needs to have 4 arguments in the call or else fail
 | 
						|
        guard args.count == 4 else {
 | 
						|
            print("Not enough arguments, 4 required: \(args.count) given")
 | 
						|
            result(FlutterError())
 | 
						|
            return
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Parse the arguments from the method call
 | 
						|
        let requireUnmeteredNetwork = args[0] as? Bool
 | 
						|
        let requireCharging = args[1] as? Bool
 | 
						|
        let triggerUpdateDelay = args[2] as? Int
 | 
						|
        let triggerMaxDelay = args[3] as? Int
 | 
						|
        
 | 
						|
        // Store the values from the call in the defaults
 | 
						|
        let defaults = UserDefaults.standard
 | 
						|
        defaults.set(requireUnmeteredNetwork, forKey: "require_unmetered_network")
 | 
						|
        defaults.set(requireCharging, forKey: "require_charging")
 | 
						|
        defaults.set(triggerUpdateDelay, forKey: "trigger_update_delay")
 | 
						|
        defaults.set(triggerMaxDelay, forKey: "trigger_max_delay")
 | 
						|
 | 
						|
        // Cancel the background services and reschedule them
 | 
						|
        BGTaskScheduler.shared.cancelAllTaskRequests()
 | 
						|
        BackgroundServicePlugin.scheduleBackgroundSync()
 | 
						|
        BackgroundServicePlugin.scheduleBackgroundFetch()
 | 
						|
        result(true)
 | 
						|
    }
 | 
						|
    
 | 
						|
    // Returns the number of currently scheduled background processes to Flutter, striclty
 | 
						|
    // for debugging
 | 
						|
    func handleNumberOfProcesses(call: FlutterMethodCall, result: @escaping FlutterResult) {
 | 
						|
        BGTaskScheduler.shared.getPendingTaskRequests { requests in
 | 
						|
            result(requests.count)
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    // Disables the service, cancels all the task requests
 | 
						|
    func handleDisable(call: FlutterMethodCall, result: FlutterResult) {
 | 
						|
        let defaults = UserDefaults.standard
 | 
						|
        defaults.set(false, forKey: "background_service_enabled")
 | 
						|
        
 | 
						|
        BGTaskScheduler.shared.cancelAllTaskRequests()
 | 
						|
        result(true)
 | 
						|
    }
 | 
						|
  
 | 
						|
    // Checks the status of the Background App Refresh from the system
 | 
						|
    // Returns true if the service is enabled for Immich, and false otherwise
 | 
						|
    func handleBackgroundRefreshStatus(call: FlutterMethodCall, result: FlutterResult) {
 | 
						|
        switch UIApplication.shared.backgroundRefreshStatus {
 | 
						|
        case .available:
 | 
						|
            result(true)
 | 
						|
            break
 | 
						|
        case .denied:
 | 
						|
            result(false)
 | 
						|
            break
 | 
						|
        case .restricted:
 | 
						|
            result(false)
 | 
						|
            break
 | 
						|
        default:
 | 
						|
            result(false)
 | 
						|
            break
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    
 | 
						|
    // Schedules a short-running background sync to sync only a few photos
 | 
						|
    static func scheduleBackgroundFetch() {
 | 
						|
        // We will schedule this task to run no matter the charging or wifi requirents from the end user
 | 
						|
        // 1. They can set Background App Refresh to Off / Wi-Fi / Wi-Fi & Cellular Data from Settings
 | 
						|
        // 2. We will check the battery connectivity when we begin running the background activity
 | 
						|
        let backgroundFetch = BGAppRefreshTaskRequest(identifier: BackgroundServicePlugin.backgroundFetchTaskID)
 | 
						|
        
 | 
						|
        // Use 5 minutes from now as earliest begin date
 | 
						|
        backgroundFetch.earliestBeginDate = Date(timeIntervalSinceNow: 5 * 60)
 | 
						|
        
 | 
						|
        do {
 | 
						|
            try BGTaskScheduler.shared.submit(backgroundFetch)
 | 
						|
        } catch {
 | 
						|
            print("Could not schedule the background task \(error.localizedDescription)")
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    // Schedules a long-running background sync for syncing all of the photos
 | 
						|
    static func scheduleBackgroundSync() {
 | 
						|
        let backgroundProcessing = BGProcessingTaskRequest(identifier: BackgroundServicePlugin.backgroundProcessingTaskID)
 | 
						|
        
 | 
						|
        // We need the values for requiring charging
 | 
						|
        let defaults = UserDefaults.standard
 | 
						|
        let requireCharging = defaults.value(forKey: "require_charging") as? Bool
 | 
						|
        
 | 
						|
        // Always require network connectivity, and set the require charging from the above
 | 
						|
        backgroundProcessing.requiresNetworkConnectivity = true
 | 
						|
        backgroundProcessing.requiresExternalPower = requireCharging ?? true
 | 
						|
                
 | 
						|
        // Use 15 minutes from now as earliest begin date
 | 
						|
        backgroundProcessing.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
 | 
						|
        
 | 
						|
        do {
 | 
						|
            // Submit the task to the scheduler
 | 
						|
            try BGTaskScheduler.shared.submit(backgroundProcessing)
 | 
						|
        } catch {
 | 
						|
            print("Could not schedule the background task \(error.localizedDescription)")
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    // This function runs when the system kicks off the BGAppRefreshTask from the Background Task Scheduler
 | 
						|
    static func handleBackgroundFetch(task: BGAppRefreshTask) {
 | 
						|
        // Schedule the next sync task so we can run this again later
 | 
						|
        scheduleBackgroundFetch()
 | 
						|
        
 | 
						|
        // Log the time of last background processing to now
 | 
						|
        let defaults = UserDefaults.standard
 | 
						|
        defaults.set(Date().timeIntervalSince1970, forKey: "last_background_fetch_run_time")
 | 
						|
        
 | 
						|
        // If we have required charging, we should check the charging status
 | 
						|
        let requireCharging = defaults.value(forKey: "require_charging") as? Bool
 | 
						|
        if (requireCharging ?? false) {
 | 
						|
            UIDevice.current.isBatteryMonitoringEnabled = true
 | 
						|
            if (UIDevice.current.batteryState == .unplugged) {
 | 
						|
                // The device is unplugged and we have required charging
 | 
						|
                // Therefore, we will simply complete the task without
 | 
						|
                // running it.
 | 
						|
                task.setTaskCompleted(success: true)
 | 
						|
                return
 | 
						|
            }
 | 
						|
        }
 | 
						|
        
 | 
						|
        // Schedule the next sync task so we can run this again later
 | 
						|
        scheduleBackgroundFetch()
 | 
						|
        
 | 
						|
        // The background sync task should only run for 20 seconds at most
 | 
						|
        BackgroundServicePlugin.runBackgroundSync(task, maxSeconds: 20)
 | 
						|
    }
 | 
						|
    
 | 
						|
    // This function runs when the system kicks off the BGProcessingTask from the Background Task Scheduler
 | 
						|
    static func handleBackgroundProcessing(task: BGProcessingTask) {
 | 
						|
        // Schedule the next sync task so we run this again later
 | 
						|
        scheduleBackgroundSync()
 | 
						|
 | 
						|
        // Log the time of last background processing to now
 | 
						|
        let defaults = UserDefaults.standard
 | 
						|
        defaults.set(Date().timeIntervalSince1970, forKey: "last_background_processing_run_time")
 | 
						|
        
 | 
						|
        // We won't specify a max time for the background sync service, so this can run for longer
 | 
						|
        BackgroundServicePlugin.runBackgroundSync(task, maxSeconds: nil)
 | 
						|
    }
 | 
						|
    
 | 
						|
    // This is a synchronous function which uses a semaphore to run the background sync worker's run
 | 
						|
    // function, which will create a background Isolate and communicate with the Flutter code to back
 | 
						|
    // up the assets. When it completes, we signal the semaphore and complete the execution allowing the
 | 
						|
    // control to pass back to the caller synchronously
 | 
						|
    static func runBackgroundSync(_ task: BGTask, maxSeconds: Int?) {
 | 
						|
 | 
						|
        let semaphore = DispatchSemaphore(value: 0)
 | 
						|
        DispatchQueue.main.async {
 | 
						|
            let backgroundWorker = BackgroundSyncWorker { _ in
 | 
						|
                semaphore.signal()
 | 
						|
            }
 | 
						|
            task.expirationHandler = {
 | 
						|
                backgroundWorker.cancel()
 | 
						|
                task.setTaskCompleted(success: true)
 | 
						|
            }
 | 
						|
            
 | 
						|
            backgroundWorker.run(maxSeconds: maxSeconds)
 | 
						|
            task.setTaskCompleted(success: true)
 | 
						|
        }
 | 
						|
        semaphore.wait()
 | 
						|
    }
 | 
						|
 | 
						|
 | 
						|
}
 |