From cf52b879b1b0797a62e039fac852c85690d9b03d Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Tue, 14 Oct 2025 07:39:12 +0530 Subject: [PATCH] fix: ios skip posting hash response after detached from engine (#22695) * skip posting message after detached from engine * review changes * cancel plugin before destroying engine --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Alex --- mobile/ios/Runner.xcodeproj/project.pbxproj | 17 +++++++++++-- mobile/ios/Runner/AppDelegate.swift | 14 +++++++---- .../Runner/Background/BackgroundWorker.swift | 3 ++- mobile/ios/Runner/Core/ImmichPlugin.swift | 17 +++++++++++++ mobile/ios/Runner/Sync/MessagesImpl.swift | 24 ++++++++++++++----- .../services/background_worker.service.dart | 2 +- 6 files changed, 62 insertions(+), 15 deletions(-) create mode 100644 mobile/ios/Runner/Core/ImmichPlugin.swift diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 6403a0ab4b..a6a7b42cca 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -131,10 +131,13 @@ /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ + B231F52D2E93A44A00BC45D1 /* Core */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = Core; + sourceTree = ""; + }; B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = { isa = PBXFileSystemSynchronizedRootGroup; - exceptions = ( - ); path = Sync; sourceTree = ""; }; @@ -247,6 +250,7 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + B231F52D2E93A44A00BC45D1 /* Core */, B25D37792E72CA15008B6CA7 /* Connectivity */, B21E34A62E5AF9760031FDB9 /* Background */, B2CF7F8C2DDE4EBB00744BF6 /* Sync */, @@ -331,6 +335,7 @@ F0B57D482DF764BE00DC5BCC /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( + B231F52D2E93A44A00BC45D1 /* Core */, B2CF7F8C2DDE4EBB00744BF6 /* Sync */, ); name = Runner; @@ -521,10 +526,14 @@ 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"; @@ -553,10 +562,14 @@ 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"; diff --git a/mobile/ios/Runner/AppDelegate.swift b/mobile/ios/Runner/AppDelegate.swift index 3476030923..4e4cb2ed13 100644 --- a/mobile/ios/Runner/AppDelegate.swift +++ b/mobile/ios/Runner/AppDelegate.swift @@ -20,7 +20,7 @@ import UIKit GeneratedPluginRegistrant.register(with: self) let controller: FlutterViewController = window?.rootViewController as! FlutterViewController - AppDelegate.registerPlugins(binaryMessenger: controller.binaryMessenger) + AppDelegate.registerPlugins(with: controller.engine) BackgroundServicePlugin.register(with: self.registrar(forPlugin: "BackgroundServicePlugin")!) BackgroundServicePlugin.registerBackgroundProcessing() @@ -51,9 +51,13 @@ import UIKit return super.application(application, didFinishLaunchingWithOptions: launchOptions) } - public static func registerPlugins(binaryMessenger: FlutterBinaryMessenger) { - NativeSyncApiSetup.setUp(binaryMessenger: binaryMessenger, api: NativeSyncApiImpl()) - ThumbnailApiSetup.setUp(binaryMessenger: binaryMessenger, api: ThumbnailApiImpl()) - BackgroundWorkerFgHostApiSetup.setUp(binaryMessenger: binaryMessenger, api: BackgroundWorkerApiImpl()) + public static func registerPlugins(with engine: FlutterEngine) { + NativeSyncApiImpl.register(with: engine.registrar(forPlugin: NativeSyncApiImpl.name)!) + ThumbnailApiSetup.setUp(binaryMessenger: engine.binaryMessenger, api: ThumbnailApiImpl()) + BackgroundWorkerFgHostApiSetup.setUp(binaryMessenger: engine.binaryMessenger, api: BackgroundWorkerApiImpl()) + } + + public static func cancelPlugins(with engine: FlutterEngine) { + (engine.valuePublished(byPlugin: NativeSyncApiImpl.name) as? NativeSyncApiImpl)?.detachFromEngine() } } diff --git a/mobile/ios/Runner/Background/BackgroundWorker.swift b/mobile/ios/Runner/Background/BackgroundWorker.swift index 15df971203..7dc450d76e 100644 --- a/mobile/ios/Runner/Background/BackgroundWorker.swift +++ b/mobile/ios/Runner/Background/BackgroundWorker.swift @@ -95,7 +95,7 @@ class BackgroundWorker: BackgroundWorkerBgHostApi { // Register plugins in the new engine GeneratedPluginRegistrant.register(with: engine) // Register custom plugins - AppDelegate.registerPlugins(binaryMessenger: engine.binaryMessenger) + AppDelegate.registerPlugins(with: engine) flutterApi = BackgroundWorkerFlutterApi(binaryMessenger: engine.binaryMessenger) BackgroundWorkerBgHostApiSetup.setUp(binaryMessenger: engine.binaryMessenger, api: self) @@ -168,6 +168,7 @@ class BackgroundWorker: BackgroundWorkerBgHostApi { } isComplete = true + AppDelegate.cancelPlugins(with: engine) engine.destroyContext() flutterApi = nil completionHandler(success) diff --git a/mobile/ios/Runner/Core/ImmichPlugin.swift b/mobile/ios/Runner/Core/ImmichPlugin.swift new file mode 100644 index 0000000000..db10b7a75d --- /dev/null +++ b/mobile/ios/Runner/Core/ImmichPlugin.swift @@ -0,0 +1,17 @@ +class ImmichPlugin: NSObject { + var detached: Bool + + override init() { + detached = false + super.init() + } + + func detachFromEngine() { + self.detached = true + } + + func completeWhenActive(for completion: @escaping (T) -> Void, with value: T) { + guard !self.detached else { return } + completion(value) + } +} diff --git a/mobile/ios/Runner/Sync/MessagesImpl.swift b/mobile/ios/Runner/Sync/MessagesImpl.swift index bb23bae6b6..c9f58398b6 100644 --- a/mobile/ios/Runner/Sync/MessagesImpl.swift +++ b/mobile/ios/Runner/Sync/MessagesImpl.swift @@ -17,13 +17,25 @@ struct AssetWrapper: Hashable, Equatable { } } -class NativeSyncApiImpl: NativeSyncApi { +class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin { + static let name = "NativeSyncApi" + + static func register(with registrar: any FlutterPluginRegistrar) { + let instance = NativeSyncApiImpl() + NativeSyncApiSetup.setUp(binaryMessenger: registrar.messenger(), api: instance) + registrar.publish(instance) + } + + func detachFromEngine(for registrar: any FlutterPluginRegistrar) { + super.detachFromEngine() + } + private let defaults: UserDefaults private let changeTokenKey = "immich:changeToken" private let albumTypes: [PHAssetCollectionType] = [.album, .smartAlbum] private let recoveredAlbumSubType = 1000000219 - private var hashTask: Task? + private var hashTask: Task? private static let hashCancelledCode = "HASH_CANCELLED" private static let hashCancelled = Result<[HashResult], Error>.failure(PigeonError(code: hashCancelledCode, message: "Hashing cancelled", details: nil)) @@ -272,7 +284,7 @@ class NativeSyncApiImpl: NativeSyncApi { } if Task.isCancelled { - return completion(Self.hashCancelled) + return self?.completeWhenActive(for: completion, with: Self.hashCancelled) } await withTaskGroup(of: HashResult?.self) { taskGroup in @@ -280,7 +292,7 @@ class NativeSyncApiImpl: NativeSyncApi { results.reserveCapacity(assets.count) for asset in assets { if Task.isCancelled { - return completion(Self.hashCancelled) + return self?.completeWhenActive(for: completion, with: Self.hashCancelled) } taskGroup.addTask { guard let self = self else { return nil } @@ -290,7 +302,7 @@ class NativeSyncApiImpl: NativeSyncApi { for await result in taskGroup { guard let result = result else { - return completion(Self.hashCancelled) + return self?.completeWhenActive(for: completion, with: Self.hashCancelled) } results.append(result) } @@ -299,7 +311,7 @@ class NativeSyncApiImpl: NativeSyncApi { results.append(HashResult(assetId: missing, error: "Asset not found in library", hash: nil)) } - completion(.success(results)) + return self?.completeWhenActive(for: completion, with: .success(results)) } } } diff --git a/mobile/lib/domain/services/background_worker.service.dart b/mobile/lib/domain/services/background_worker.service.dart index d95b1d4951..78ba5b7088 100644 --- a/mobile/lib/domain/services/background_worker.service.dart +++ b/mobile/lib/domain/services/background_worker.service.dart @@ -192,6 +192,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { _cancellationToken.cancel(); _logger.info("Cleaning up background worker"); final cleanupFutures = [ + nativeSyncApi?.cancelHashing(), workerManager.dispose().catchError((_) async { // Discard any errors on the dispose call return; @@ -201,7 +202,6 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { _drift.close(), _driftLogger.close(), backgroundSyncManager?.cancel(), - nativeSyncApi?.cancelHashing(), ]; if (_isar.isOpen) {