refactor: yeet old timeline (#27666)

* refactor: yank old timeline

# Conflicts:
#	mobile/lib/presentation/pages/editing/drift_edit.page.dart
#	mobile/lib/providers/websocket.provider.dart
#	mobile/lib/routing/router.dart

* more cleanup

* remove native code

* chore: bump sqlite-data version

* remove old background tasks from BGTaskSchedulerPermittedIdentifiers

* rebase

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
shenlong
2026-04-15 23:00:27 +05:30
committed by GitHub
parent 6dd6053222
commit 79fccdbee0
367 changed files with 332 additions and 50870 deletions
+8 -14
View File
@@ -33,8 +33,6 @@ PODS:
- Flutter
- integration_test (0.0.1):
- Flutter
- isar_community_flutter_libs (1.0.0):
- Flutter
- local_auth_darwin (0.0.1):
- Flutter
- FlutterMacOS
@@ -75,16 +73,16 @@ PODS:
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- sqlite3 (3.49.1):
- sqlite3/common (= 3.49.1)
- sqlite3/common (3.49.1)
- sqlite3/dbstatvtab (3.49.1):
- sqlite3 (3.49.2):
- sqlite3/common (= 3.49.2)
- sqlite3/common (3.49.2)
- sqlite3/dbstatvtab (3.49.2):
- sqlite3/common
- sqlite3/fts5 (3.49.1):
- sqlite3/fts5 (3.49.2):
- sqlite3/common
- sqlite3/perf-threadsafe (3.49.1):
- sqlite3/perf-threadsafe (3.49.2):
- sqlite3/common
- sqlite3/rtree (3.49.1):
- sqlite3/rtree (3.49.2):
- sqlite3/common
- sqlite3_flutter_libs (0.0.1):
- Flutter
@@ -116,7 +114,6 @@ DEPENDENCIES:
- home_widget (from `.symlinks/plugins/home_widget/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- integration_test (from `.symlinks/plugins/integration_test/ios`)
- isar_community_flutter_libs (from `.symlinks/plugins/isar_community_flutter_libs/ios`)
- local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
- maplibre_gl (from `.symlinks/plugins/maplibre_gl/ios`)
- native_video_player (from `.symlinks/plugins/native_video_player/ios`)
@@ -174,8 +171,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/image_picker_ios/ios"
integration_test:
:path: ".symlinks/plugins/integration_test/ios"
isar_community_flutter_libs:
:path: ".symlinks/plugins/isar_community_flutter_libs/ios"
local_auth_darwin:
:path: ".symlinks/plugins/local_auth_darwin/darwin"
maplibre_gl:
@@ -228,7 +223,6 @@ SPEC CHECKSUMS:
home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
isar_community_flutter_libs: bede843185a61a05ff364a05c9b23209523f7e0d
local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391
MapLibre: 69e572367f4ef6287e18246cfafc39c80cdcabcd
maplibre_gl: 3c924e44725147b03dda33430ad216005b40555f
@@ -245,7 +239,7 @@ SPEC CHECKSUMS:
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
sqlite3: 3c950dc86011117c307eb0b28c4a7bb449dce9f1
sqlite3_flutter_libs: f8fc13346870e73fe35ebf6dbb997fbcd156b241
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
+8 -26
View File
@@ -10,8 +10,6 @@
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
3B6A31FED0FC846D6BD69BBC /* Pods_ShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 357FC57E54FD0F51795CF28A /* Pods_ShareExtension.framework */; };
65F32F31299BD2F800CE9261 /* BackgroundServicePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65F32F30299BD2F800CE9261 /* BackgroundServicePlugin.swift */; };
65F32F33299D349D00CE9261 /* BackgroundSyncWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65F32F32299D349D00CE9261 /* BackgroundSyncWorker.swift */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
@@ -90,8 +88,6 @@
357FC57E54FD0F51795CF28A /* Pods_ShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
571EAA93D77181C7C98C2EA6 /* Pods-ShareExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.release.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.release.xcconfig"; sourceTree = "<group>"; };
65F32F30299BD2F800CE9261 /* BackgroundServicePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundServicePlugin.swift; sourceTree = "<group>"; };
65F32F32299D349D00CE9261 /* BackgroundSyncWorker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundSyncWorker.swift; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
@@ -151,11 +147,15 @@
/* Begin PBXFileSystemSynchronizedRootGroup section */
B231F52D2E93A44A00BC45D1 /* Core */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
path = Core;
sourceTree = "<group>";
};
B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
path = Sync;
sourceTree = "<group>";
};
@@ -177,6 +177,8 @@
};
FEE084F22EC172080045228E /* Schemas */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
path = Schemas;
sourceTree = "<group>";
};
@@ -238,15 +240,6 @@
name = Frameworks;
sourceTree = "<group>";
};
65DD438629917FAD0047FFA8 /* BackgroundSync */ = {
isa = PBXGroup;
children = (
65F32F32299D349D00CE9261 /* BackgroundSyncWorker.swift */,
65F32F30299BD2F800CE9261 /* BackgroundServicePlugin.swift */,
);
path = BackgroundSync;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
@@ -291,7 +284,6 @@
B21E34A62E5AF9760031FDB9 /* Background */,
B2CF7F8C2DDE4EBB00744BF6 /* Sync */,
FA9973382CF6DF4B000EF859 /* Runner.entitlements */,
65DD438629917FAD0047FFA8 /* BackgroundSync */,
FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
@@ -571,14 +563,10 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
@@ -607,14 +595,10 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
@@ -627,7 +611,6 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
65F32F31299BD2F800CE9261 /* BackgroundServicePlugin.swift in Sources */,
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
A01DD69B2F7F43B40049AB63 /* ImageRequest.swift in Sources */,
B21E34AC2E5B09190031FDB9 /* BackgroundWorker.swift in Sources */,
@@ -642,7 +625,6 @@
B21E34AA2E5AFD2B0031FDB9 /* BackgroundWorkerApiImpl.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
B2BE315F2E5E5229006EEF88 /* BackgroundWorker.g.swift in Sources */,
65F32F33299D349D00CE9261 /* BackgroundSyncWorker.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1261,7 +1243,7 @@
repositoryURL = "https://github.com/pointfreeco/sqlite-data";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.3.0;
minimumVersion = 1.6.1;
};
};
FEE084F92EC1725A0045228E /* XCRemoteSwiftPackageReference "swift-http-structured-headers" */ = {
@@ -1269,7 +1251,7 @@
repositoryURL = "https://github.com/apple/swift-http-structured-headers.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.5.0;
minimumVersion = 1.6.0;
};
};
/* End XCRemoteSwiftPackageReference section */
@@ -24,8 +24,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/sqlite-data",
"state" : {
"revision" : "05704b563ecb7f0bd7e49b6f360a6383a3e53e7d",
"version" : "1.5.1"
"revision" : "da3a94ed49c7a30d82853de551c07a93196e8cab",
"version" : "1.6.1"
}
},
{
@@ -78,8 +78,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-http-structured-headers.git",
"state" : {
"revision" : "a9f3c352f4d46afd155e00b3c6e85decae6bcbeb",
"version" : "1.5.0"
"revision" : "76d7627bd88b47bf5a0f8497dd244885960dde0b",
"version" : "1.6.0"
}
},
{
@@ -123,8 +123,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-structured-queries",
"state" : {
"revision" : "d8163b3a98f3c8434c4361e85126db449d84bc66",
"version" : "0.30.0"
"revision" : "8da8818fccd9959bd683934ddc62cf45bb65b3c8",
"version" : "0.31.1"
}
},
{
-25
View File
@@ -24,33 +24,8 @@ import UIKit
GeneratedPluginRegistrant.register(with: self)
let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
AppDelegate.registerPlugins(with: controller.engine, controller: controller)
BackgroundServicePlugin.register(with: self.registrar(forPlugin: "BackgroundServicePlugin")!)
BackgroundServicePlugin.registerBackgroundProcessing()
BackgroundWorkerApiImpl.registerBackgroundWorkers()
BackgroundServicePlugin.setPluginRegistrantCallback { registry in
if !registry.hasPlugin("org.cocoapods.path-provider-foundation") {
PathProviderPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.path-provider-foundation")!)
}
if !registry.hasPlugin("org.cocoapods.photo-manager") {
PhotoManagerPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.photo-manager")!)
}
if !registry.hasPlugin("org.cocoapods.shared-preferences-foundation") {
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.shared-preferences-foundation")!)
}
if !registry.hasPlugin("org.cocoapods.permission-handler-apple") {
PermissionHandlerPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.permission-handler-apple")!)
}
if !registry.hasPlugin("org.cocoapods.network-info-plus") {
FPPNetworkInfoPlusPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.network-info-plus")!)
}
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
@@ -1,408 +0,0 @@
//
// BackgroundServicePlugin.swift
// Runner
//
// Created by Marty Fuhry on 2/14/23.
//
import Flutter
import BackgroundTasks
import path_provider_foundation
import CryptoKit
import Network
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
case "digestFiles":
handleDigestFiles(call: call, result: result)
break
default:
result(FlutterMethodNotImplemented)
break
}
}
// Calculates the SHA-1 hash of each file from the list of paths provided
func handleDigestFiles(call: FlutterMethodCall, result: @escaping FlutterResult) {
let bufsize = 2 * 1024 * 1024
// Private error to throw if file cannot be read
enum DigestError: String, LocalizedError {
case NoFileHandle = "Cannot Open File Handle"
public var errorDescription: String? { self.rawValue }
}
// Parse the arguments or else fail
guard let args = call.arguments as? Array<String> else {
print("Cannot parse args as array: \(String(describing: call.arguments))")
result(FlutterError(code: "Malformed",
message: "Received args is not an Array<String>",
details: nil))
return
}
// Compute hash in background thread
DispatchQueue.global(qos: .background).async {
var hashes: [FlutterStandardTypedData?] = Array(repeating: nil, count: args.count)
for i in (0 ..< args.count) {
do {
guard let file = FileHandle(forReadingAtPath: args[i]) else { throw DigestError.NoFileHandle }
var hasher = Insecure.SHA1.init();
while autoreleasepool(invoking: {
let chunk = file.readData(ofLength: bufsize)
guard !chunk.isEmpty else { return false } // EOF
hasher.update(data: chunk)
return true // continue
}) { }
let digest = hasher.finalize()
hashes[i] = FlutterStandardTypedData(bytes: Data(Array(digest.makeIterator())))
} catch {
print("Cannot calculate the digest of the file \(args[i]) due to \(error.localizedDescription)")
}
}
// Return result in main thread
DispatchQueue.main.async {
result(Array(hashes))
}
}
}
// Called by the flutter code when enabled so that we can turn on the background 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, strictly
// 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 ?? false
if (requireCharging) {
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
}
}
// If we have required Wi-Fi, we can check the isExpensive property
let requireWifi = defaults.value(forKey: "require_wifi") as? Bool ?? false
if (requireWifi) {
let wifiMonitor = NWPathMonitor(requiredInterfaceType: .wifi)
let isExpensive = wifiMonitor.currentPath.isExpensive
if (isExpensive) {
// The network is expensive and we have required Wi-Fi
// 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()
}
}
@@ -1,271 +0,0 @@
//
// 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])
}
}
-2
View File
@@ -8,8 +8,6 @@
<array>
<string>app.alextran.immich.background.refreshUpload</string>
<string>app.alextran.immich.background.processingUpload</string>
<string>app.alextran.immich.backgroundFetch</string>
<string>app.alextran.immich.backgroundProcessing</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>