diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.g.kt index 39a2345a9b..3d48b8be5e 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.g.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.g.kt @@ -62,7 +62,7 @@ private open class BackgroundWorkerPigeonCodec : StandardMessageCodec() { /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ interface BackgroundWorkerFgHostApi { fun enableSyncWorker() - fun enableUploadWorker(callbackHandle: Long) + fun enableUploadWorker() fun disableUploadWorker() companion object { @@ -93,11 +93,9 @@ interface BackgroundWorkerFgHostApi { run { val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableUploadWorker$separatedMessageChannelSuffix", codec) if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val callbackHandleArg = args[0] as Long + channel.setMessageHandler { _, reply -> val wrapped: List = try { - api.enableUploadWorker(callbackHandleArg) + api.enableUploadWorker() listOf(null) } catch (exception: Throwable) { BackgroundWorkerPigeonUtils.wrapError(exception) @@ -130,6 +128,7 @@ interface BackgroundWorkerFgHostApi { /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ interface BackgroundWorkerBgHostApi { fun onInitialized() + fun close() companion object { /** The codec used by BackgroundWorkerBgHostApi. */ @@ -156,6 +155,22 @@ interface BackgroundWorkerBgHostApi { channel.setMessageHandler(null) } } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.close$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + api.close() + listOf(null) + } catch (exception: Throwable) { + BackgroundWorkerPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } } } } diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.kt index 0ce601b363..ed96b44769 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.kt @@ -11,9 +11,8 @@ import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.SettableFuture import io.flutter.FlutterInjector import io.flutter.embedding.engine.FlutterEngine -import io.flutter.embedding.engine.dart.DartExecutor.DartCallback +import io.flutter.embedding.engine.dart.DartExecutor import io.flutter.embedding.engine.loader.FlutterLoader -import io.flutter.view.FlutterCallbackInformation private const val TAG = "BackgroundWorker" @@ -58,25 +57,6 @@ class BackgroundWorker(context: Context, params: WorkerParameters) : loader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) { engine = FlutterEngine(ctx) - // Retrieve the callback handle stored by the main Flutter app - // This handle points to the Flutter function that should be executed in the background - val callbackHandle = - ctx.getSharedPreferences(BackgroundWorkerApiImpl.SHARED_PREF_NAME, Context.MODE_PRIVATE) - .getLong(BackgroundWorkerApiImpl.SHARED_PREF_CALLBACK_HANDLE, 0L) - - if (callbackHandle == 0L) { - // Without a valid callback handle, we cannot start the Flutter background execution - complete(Result.failure()) - return@ensureInitializationCompleteAsync - } - - // Start the Flutter engine with the specified callback as the entry point - val callback = FlutterCallbackInformation.lookupCallbackInformation(callbackHandle) - if (callback == null) { - complete(Result.failure()) - return@ensureInitializationCompleteAsync - } - // Register custom plugins MainActivity.registerPlugins(ctx, engine!!) flutterApi = @@ -86,8 +66,12 @@ class BackgroundWorker(context: Context, params: WorkerParameters) : api = this ) - engine!!.dartExecutor.executeDartCallback( - DartCallback(ctx.assets, loader.findAppBundlePath(), callback) + engine!!.dartExecutor.executeDartEntrypoint( + DartExecutor.DartEntrypoint( + loader.findAppBundlePath(), + "package:immich_mobile/domain/services/background_worker.service.dart", + "backgroundSyncNativeEntrypoint" + ) ) } @@ -109,14 +93,7 @@ class BackgroundWorker(context: Context, params: WorkerParameters) : } } - /** - * Called when the system has to stop this worker because constraints are - * no longer met or the system needs resources for more important tasks - * This is also called when the worker has been explicitly cancelled or replaced - */ - override fun onStopped() { - Log.d(TAG, "About to stop BackupWorker") - + override fun close() { if (isComplete) { return } @@ -134,6 +111,16 @@ class BackgroundWorker(context: Context, params: WorkerParameters) : }, 5000) } + /** + * Called when the system has to stop this worker because constraints are + * no longer met or the system needs resources for more important tasks + * This is also called when the worker has been explicitly cancelled or replaced + */ + override fun onStopped() { + Log.d(TAG, "About to stop BackupWorker") + close() + } + private fun handleHostResult(result: kotlin.Result) { if (isComplete) { return diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorkerApiImpl.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorkerApiImpl.kt index 7a3226f961..6cd4fbe0bf 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorkerApiImpl.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorkerApiImpl.kt @@ -21,9 +21,8 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi { Log.i(TAG, "Scheduled media observer") } - override fun enableUploadWorker(callbackHandle: Long) { + override fun enableUploadWorker() { updateUploadEnabled(ctx, true) - updateCallbackHandle(ctx, callbackHandle) Log.i(TAG, "Scheduled background upload tasks") } @@ -41,7 +40,6 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi { const val SHARED_PREF_NAME = "Immich::Background" const val SHARED_PREF_BACKUP_ENABLED = "Background::backup::enabled" - const val SHARED_PREF_CALLBACK_HANDLE = "Background::backup::callbackHandle" private fun updateUploadEnabled(context: Context, enabled: Boolean) { context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE).edit { @@ -49,12 +47,6 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi { } } - private fun updateCallbackHandle(context: Context, callbackHandle: Long) { - context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE).edit { - putLong(SHARED_PREF_CALLBACK_HANDLE, callbackHandle) - } - } - fun enqueueMediaObserver(ctx: Context) { val constraints = Constraints.Builder() .addContentUriTrigger(MediaStore.Images.Media.INTERNAL_CONTENT_URI, true) diff --git a/mobile/ios/Runner/AppDelegate.swift b/mobile/ios/Runner/AppDelegate.swift index 04422eb2b4..3476030923 100644 --- a/mobile/ios/Runner/AppDelegate.swift +++ b/mobile/ios/Runner/AppDelegate.swift @@ -24,7 +24,7 @@ import UIKit BackgroundServicePlugin.register(with: self.registrar(forPlugin: "BackgroundServicePlugin")!) BackgroundServicePlugin.registerBackgroundProcessing() - BackgroundWorkerApiImpl.registerBackgroundProcessing() + BackgroundWorkerApiImpl.registerBackgroundWorkers() BackgroundServicePlugin.setPluginRegistrantCallback { registry in if !registry.hasPlugin("org.cocoapods.path-provider-foundation") { diff --git a/mobile/ios/Runner/Background/BackgroundWorker.g.swift b/mobile/ios/Runner/Background/BackgroundWorker.g.swift index e9513db8da..b81a5f7576 100644 --- a/mobile/ios/Runner/Background/BackgroundWorker.g.swift +++ b/mobile/ios/Runner/Background/BackgroundWorker.g.swift @@ -74,7 +74,7 @@ class BackgroundWorkerPigeonCodec: FlutterStandardMessageCodec, @unchecked Senda /// Generated protocol from Pigeon that represents a handler of messages from Flutter. protocol BackgroundWorkerFgHostApi { func enableSyncWorker() throws - func enableUploadWorker(callbackHandle: Int64) throws + func enableUploadWorker() throws func disableUploadWorker() throws } @@ -99,11 +99,9 @@ class BackgroundWorkerFgHostApiSetup { } let enableUploadWorkerChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableUploadWorker\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { - enableUploadWorkerChannel.setMessageHandler { message, reply in - let args = message as! [Any?] - let callbackHandleArg = args[0] as! Int64 + enableUploadWorkerChannel.setMessageHandler { _, reply in do { - try api.enableUploadWorker(callbackHandle: callbackHandleArg) + try api.enableUploadWorker() reply(wrapResult(nil)) } catch { reply(wrapError(error)) @@ -130,6 +128,7 @@ class BackgroundWorkerFgHostApiSetup { /// Generated protocol from Pigeon that represents a handler of messages from Flutter. protocol BackgroundWorkerBgHostApi { func onInitialized() throws + func close() throws } /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. @@ -151,6 +150,19 @@ class BackgroundWorkerBgHostApiSetup { } else { onInitializedChannel.setMessageHandler(nil) } + let closeChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.close\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + closeChannel.setMessageHandler { _, reply in + do { + try api.close() + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + closeChannel.setMessageHandler(nil) + } } } /// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift. diff --git a/mobile/ios/Runner/Background/BackgroundWorker.swift b/mobile/ios/Runner/Background/BackgroundWorker.swift index db849d942b..fb0fed6b5c 100644 --- a/mobile/ios/Runner/Background/BackgroundWorker.swift +++ b/mobile/ios/Runner/Background/BackgroundWorker.swift @@ -86,28 +86,10 @@ class BackgroundWorker: BackgroundWorkerBgHostApi { * starts the engine, and sets up a timeout timer if specified. */ func run() { - // Retrieve the callback handle stored by the main Flutter app - // This handle points to the Flutter function that should be executed in the background - let callbackHandle = Int64(UserDefaults.standard.string( - forKey: BackgroundWorkerApiImpl.backgroundUploadCallbackHandleKey) ?? "0") ?? 0 - - if callbackHandle == 0 { - // Without a valid callback handle, we cannot start the Flutter background execution - complete(success: false) - return - } - - // Use the callback handle to retrieve the actual Flutter callback information - guard let callback = FlutterCallbackCache.lookupCallbackInformation(callbackHandle) else { - // The callback handle is invalid or the callback was not found - complete(success: false) - return - } - // Start the Flutter engine with the specified callback as the entry point let isRunning = engine.run( - withEntrypoint: callback.callbackName, - libraryURI: callback.callbackLibraryPath + withEntrypoint: "backgroundSyncNativeEntrypoint", + libraryURI: "package:immich_mobile/domain/services/background_worker.service.dart" ) // Verify that the Flutter engine started successfully @@ -127,7 +109,7 @@ class BackgroundWorker: BackgroundWorkerBgHostApi { if maxSeconds != nil { // Schedule a timer to cancel the task after the specified timeout period Timer.scheduledTimer(withTimeInterval: TimeInterval(maxSeconds!), repeats: false) { _ in - self.cancel() + self.close() } } } @@ -156,7 +138,7 @@ class BackgroundWorker: BackgroundWorkerBgHostApi { * Sends a cancel signal to the Flutter side and sets up a fallback timer to ensure * the completion handler is eventually called even if Flutter doesn't respond. */ - func cancel() { + func close() { if isComplete { return } @@ -182,7 +164,7 @@ class BackgroundWorker: BackgroundWorkerBgHostApi { private func handleHostResult(result: Result) { switch result { case .success(): self.complete(success: true) - case .failure(_): self.cancel() + case .failure(_): self.close() } } diff --git a/mobile/ios/Runner/Background/BackgroundWorkerApiImpl.swift b/mobile/ios/Runner/Background/BackgroundWorkerApiImpl.swift index f36085de0b..0bc46ff6b2 100644 --- a/mobile/ios/Runner/Background/BackgroundWorkerApiImpl.swift +++ b/mobile/ios/Runner/Background/BackgroundWorkerApiImpl.swift @@ -6,10 +6,8 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi { print("BackgroundUploadImpl:enableSyncWorker Local Sync worker scheduled") } - func enableUploadWorker(callbackHandle: Int64) throws { + func enableUploadWorker() throws { BackgroundWorkerApiImpl.updateUploadEnabled(true) - // Store the callback handle for later use when starting background Flutter isolates - BackgroundWorkerApiImpl.updateUploadCallbackHandle(callbackHandle) BackgroundWorkerApiImpl.scheduleRefreshUpload() BackgroundWorkerApiImpl.scheduleProcessingUpload() @@ -23,7 +21,6 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi { } public static let backgroundUploadEnabledKey = "immich:background:backup:enabled" - public static let backgroundUploadCallbackHandleKey = "immich:background:backup:callbackHandle" private static let localSyncTaskID = "app.alextran.immich.background.localSync" private static let refreshUploadTaskID = "app.alextran.immich.background.refreshUpload" @@ -33,17 +30,13 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi { return UserDefaults.standard.set(isEnabled, forKey: BackgroundWorkerApiImpl.backgroundUploadEnabledKey) } - private static func updateUploadCallbackHandle(_ callbackHandle: Int64) { - return UserDefaults.standard.set(String(callbackHandle), forKey: BackgroundWorkerApiImpl.backgroundUploadCallbackHandleKey) - } - private static func cancelUploadTasks() { BackgroundWorkerApiImpl.updateUploadEnabled(false) BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: refreshUploadTaskID); BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: processingUploadTaskID); } - public static func registerBackgroundProcessing() { + public static func registerBackgroundWorkers() { BGTaskScheduler.shared.register( forTaskWithIdentifier: processingUploadTaskID, using: nil) { task in if task is BGProcessingTask { @@ -102,9 +95,22 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi { } private static func handleBackgroundRefresh(task: BGAppRefreshTask, taskType: BackgroundTaskType) { - scheduleRefreshUpload() - // Restrict the refresh task to run only for a maximum of 20 seconds - runBackgroundWorker(task: task, taskType: taskType, maxSeconds: 20) + let maxSeconds: Int? + + switch taskType { + case .localSync: + maxSeconds = 15 + scheduleLocalSync() + case .refreshUpload: + maxSeconds = 20 + scheduleRefreshUpload() + case .processingUpload: + print("Unexpected background refresh task encountered") + return; + } + + // Restrict the refresh task to run only for a maximum of (maxSeconds) seconds + runBackgroundWorker(task: task, taskType: taskType, maxSeconds: maxSeconds) } private static func handleBackgroundProcessing(task: BGProcessingTask) { @@ -134,7 +140,7 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi { task.expirationHandler = { DispatchQueue.main.async { - backgroundWorker.cancel() + backgroundWorker.close() } isSuccess = false diff --git a/mobile/lib/domain/services/background_worker.service.dart b/mobile/lib/domain/services/background_worker.service.dart index 33c58cf743..b237840b75 100644 --- a/mobile/lib/domain/services/background_worker.service.dart +++ b/mobile/lib/domain/services/background_worker.service.dart @@ -14,6 +14,7 @@ import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/repositories/file_media.repository.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/services/auth.service.dart'; import 'package:immich_mobile/services/localization.service.dart'; @@ -31,9 +32,7 @@ class BackgroundWorkerFgService { // TODO: Move this call to native side once old timeline is removed Future enableSyncService() => _foregroundHostApi.enableSyncWorker(); - Future enableUploadService() => _foregroundHostApi.enableUploadWorker( - PluginUtilities.getCallbackHandle(_backgroundSyncNativeEntrypoint)!.toRawHandle(), - ); + Future enableUploadService() => _foregroundHostApi.enableUploadWorker(); Future disableUploadService() => _foregroundHostApi.disableUploadWorker(); } @@ -44,7 +43,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { final Drift _drift; final DriftLogger _driftLogger; final BackgroundWorkerBgHostApi _backgroundHostApi; - final Logger _logger = Logger('BackgroundUploadBgService'); + final Logger _logger = Logger('BackgroundWorkerBgService'); bool _isCleanedUp = false; @@ -66,37 +65,50 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { bool get _isBackupEnabled => _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup); Future init() async { - await loadTranslations(); - HttpSSLOptions.apply(applyNative: false); - await _ref.read(authServiceProvider).setOpenApiServiceEndpoint(); + try { + await loadTranslations(); + HttpSSLOptions.apply(applyNative: false); + await _ref.read(authServiceProvider).setOpenApiServiceEndpoint(); - // Initialize the file downloader - await FileDownloader().configure( - globalConfig: [ - // maxConcurrent: 6, maxConcurrentByHost(server):6, maxConcurrentByGroup: 3 - (Config.holdingQueue, (6, 6, 3)), - // On Android, if files are larger than 256MB, run in foreground service - (Config.runInForegroundIfFileLargerThan, 256), - ], - ); - await FileDownloader().trackTasksInGroup(kDownloadGroupLivePhoto, markDownloadedComplete: false); - await FileDownloader().trackTasks(); - configureFileDownloaderNotifications(); + // Initialize the file downloader + await FileDownloader().configure( + globalConfig: [ + // maxConcurrent: 6, maxConcurrentByHost(server):6, maxConcurrentByGroup: 3 + (Config.holdingQueue, (6, 6, 3)), + // On Android, if files are larger than 256MB, run in foreground service + (Config.runInForegroundIfFileLargerThan, 256), + ], + ); + await FileDownloader().trackTasksInGroup(kDownloadGroupLivePhoto, markDownloadedComplete: false); + await FileDownloader().trackTasks(); + configureFileDownloaderNotifications(); - // Notify the host that the background upload service has been initialized and is ready to use - await _backgroundHostApi.onInitialized(); + await _ref.read(fileMediaRepositoryProvider).enableBackgroundAccess(); + + // Notify the host that the background worker service has been initialized and is ready to use + _backgroundHostApi.onInitialized(); + } catch (error, stack) { + _logger.severe("Failed to initialize background worker", error, stack); + _backgroundHostApi.close(); + } } @override Future onLocalSync(int? maxSeconds) async { - _logger.info('Local background syncing started'); - final sw = Stopwatch()..start(); + try { + _logger.info('Local background syncing started'); + final sw = Stopwatch()..start(); - final timeout = maxSeconds != null ? Duration(seconds: maxSeconds) : null; - await _syncAssets(hashTimeout: timeout, syncRemote: false); + final timeout = maxSeconds != null ? Duration(seconds: maxSeconds) : null; + await _syncAssets(hashTimeout: timeout, syncRemote: false); - sw.stop(); - _logger.info("Local sync completed in ${sw.elapsed.inSeconds}s"); + sw.stop(); + _logger.info("Local sync completed in ${sw.elapsed.inSeconds}s"); + } catch (error, stack) { + _logger.severe("Failed to complete local sync", error, stack); + } finally { + await _cleanup(); + } } /* We do the following on Android upload @@ -107,16 +119,20 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { */ @override Future onAndroidUpload() async { - _logger.info('Android background processing started'); - final sw = Stopwatch()..start(); + try { + _logger.info('Android background processing started'); + final sw = Stopwatch()..start(); - await _syncAssets(hashTimeout: Duration(minutes: _isBackupEnabled ? 3 : 6)); - await _handleBackup(processBulk: false); + await _syncAssets(hashTimeout: Duration(minutes: _isBackupEnabled ? 3 : 6)); + await _handleBackup(processBulk: false); - await _cleanup(); - - sw.stop(); - _logger.info("Android background processing completed in ${sw.elapsed.inSeconds}s"); + sw.stop(); + _logger.info("Android background processing completed in ${sw.elapsed.inSeconds}s"); + } catch (error, stack) { + _logger.severe("Failed to complete Android background processing", error, stack); + } finally { + await _cleanup(); + } } /* We do the following on background upload @@ -129,29 +145,37 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { */ @override Future onIosUpload(bool isRefresh, int? maxSeconds) async { - _logger.info('iOS background upload started with maxSeconds: ${maxSeconds}s'); - final sw = Stopwatch()..start(); + try { + _logger.info('iOS background upload started with maxSeconds: ${maxSeconds}s'); + final sw = Stopwatch()..start(); - final timeout = isRefresh ? const Duration(seconds: 5) : Duration(minutes: _isBackupEnabled ? 3 : 6); - await _syncAssets(hashTimeout: timeout); + final timeout = isRefresh ? const Duration(seconds: 5) : Duration(minutes: _isBackupEnabled ? 3 : 6); + await _syncAssets(hashTimeout: timeout); - final backupFuture = _handleBackup(); - if (maxSeconds != null) { - await backupFuture.timeout(Duration(seconds: maxSeconds - 1), onTimeout: () {}); - } else { - await backupFuture; + final backupFuture = _handleBackup(); + if (maxSeconds != null) { + await backupFuture.timeout(Duration(seconds: maxSeconds - 1), onTimeout: () {}); + } else { + await backupFuture; + } + + sw.stop(); + _logger.info("iOS background upload completed in ${sw.elapsed.inSeconds}s"); + } catch (error, stack) { + _logger.severe("Failed to complete iOS background upload", error, stack); + } finally { + await _cleanup(); } - - await _cleanup(); - - sw.stop(); - _logger.info("iOS background upload completed in ${sw.elapsed.inSeconds}s"); } @override Future cancel() async { - _logger.warning("Background upload cancelled"); - await _cleanup(); + _logger.warning("Background worker cancelled"); + try { + await _cleanup(); + } catch (error, stack) { + debugPrint('Failed to cleanup background worker: $error with stack: $stack'); + } } Future _cleanup() async { @@ -159,13 +183,21 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { return; } - _isCleanedUp = true; - await _ref.read(backgroundSyncProvider).cancel(); - await _ref.read(backgroundSyncProvider).cancelLocal(); - await _isar.close(); - await _drift.close(); - await _driftLogger.close(); - _ref.dispose(); + try { + _isCleanedUp = true; + _logger.info("Cleaning up background worker"); + await _ref.read(backgroundSyncProvider).cancel(); + await _ref.read(backgroundSyncProvider).cancelLocal(); + if (_isar.isOpen) { + await _isar.close(); + } + await _drift.close(); + await _driftLogger.close(); + _ref.dispose(); + debugPrint("Background worker cleaned up"); + } catch (error, stack) { + debugPrint('Failed to cleanup background worker: $error with stack: $stack'); + } } Future _handleBackup({bool processBulk = true}) async { @@ -221,8 +253,10 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { } } +/// Native entry invoked from the background worker. If renaming or moving this to a different +/// library, make sure to update the entry points and URI in native workers as well @pragma('vm:entry-point') -Future _backgroundSyncNativeEntrypoint() async { +Future backgroundSyncNativeEntrypoint() async { WidgetsFlutterBinding.ensureInitialized(); DartPluginRegistrant.ensureInitialized(); diff --git a/mobile/lib/infrastructure/repositories/storage.repository.dart b/mobile/lib/infrastructure/repositories/storage.repository.dart index 18302aeb7d..164fa04529 100644 --- a/mobile/lib/infrastructure/repositories/storage.repository.dart +++ b/mobile/lib/infrastructure/repositories/storage.repository.dart @@ -16,6 +16,13 @@ class StorageRepository { file = await entity?.originFile; if (file == null) { log.warning("Cannot get file for asset $assetId"); + return null; + } + + final exists = await file.exists(); + if (!exists) { + log.warning("File for asset $assetId does not exist"); + return null; } } catch (error, stackTrace) { log.warning("Error getting file for asset $assetId", error, stackTrace); @@ -34,6 +41,13 @@ class StorageRepository { log.warning( "Cannot get motion file for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt}", ); + return null; + } + + final exists = await file.exists(); + if (!exists) { + log.warning("Motion file for asset ${asset.id} does not exist"); + return null; } } catch (error, stackTrace) { log.warning( diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 207e522587..afc9c13181 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -206,14 +206,14 @@ class ImmichAppState extends ConsumerState with WidgetsBindingObserve WidgetsBinding.instance.addPostFrameCallback((_) { // needs to be delayed so that EasyLocalization is working if (Store.isBetaTimelineEnabled) { + ref.read(backgroundServiceProvider).disableService(); ref.read(driftBackgroundUploadFgService).enableSyncService(); if (ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup)) { - ref.read(backgroundServiceProvider).disableService(); ref.read(driftBackgroundUploadFgService).enableUploadService(); } } else { - ref.read(backgroundServiceProvider).resumeServiceIfEnabled(); ref.read(driftBackgroundUploadFgService).disableUploadService(); + ref.read(backgroundServiceProvider).resumeServiceIfEnabled(); } }); diff --git a/mobile/lib/platform/background_worker_api.g.dart b/mobile/lib/platform/background_worker_api.g.dart index 646eb63b76..12e4d2c0c5 100644 --- a/mobile/lib/platform/background_worker_api.g.dart +++ b/mobile/lib/platform/background_worker_api.g.dart @@ -82,7 +82,7 @@ class BackgroundWorkerFgHostApi { } } - Future enableUploadWorker(int callbackHandle) async { + Future enableUploadWorker() async { final String pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableUploadWorker$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( @@ -90,7 +90,7 @@ class BackgroundWorkerFgHostApi { pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([callbackHandle]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -164,6 +164,29 @@ class BackgroundWorkerBgHostApi { return; } } + + Future close() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.close$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } } abstract class BackgroundWorkerFlutterApi { diff --git a/mobile/pigeon/background_worker_api.dart b/mobile/pigeon/background_worker_api.dart index eb1b7a2c5e..b0c785f2e1 100644 --- a/mobile/pigeon/background_worker_api.dart +++ b/mobile/pigeon/background_worker_api.dart @@ -15,8 +15,7 @@ import 'package:pigeon/pigeon.dart'; abstract class BackgroundWorkerFgHostApi { void enableSyncWorker(); - // Enables the background upload service with the given callback handle - void enableUploadWorker(int callbackHandle); + void enableUploadWorker(); // Disables the background upload service void disableUploadWorker(); @@ -27,6 +26,8 @@ abstract class BackgroundWorkerBgHostApi { // Called from the background flutter engine when it has bootstrapped and established the // required platform channels to notify the native side to start the background upload void onInitialized(); + + void close(); } @FlutterApi()