mirror of
https://github.com/immich-app/immich.git
synced 2024-11-17 02:49:01 -07:00
e9c9b7a3e2
* 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])
|
|
}
|
|
}
|
|
|