fix: local sync task never runs on iOS (#21491)

* fix: local sync task never runs on iOS

* chore: rename ios register method

* refactor from using dart callback to dart entrypoint + more logs

* check if file exists before hashing

* reschedule local sync task

* chore: rename background worker logger

* refactor: move file exists check inside repo

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
shenlong 2025-09-03 02:05:58 +05:30 committed by GitHub
parent 4f7702c6bf
commit 674faf2e57
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 218 additions and 152 deletions

View file

@ -62,7 +62,7 @@ private open class BackgroundWorkerPigeonCodec : StandardMessageCodec() {
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ /** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface BackgroundWorkerFgHostApi { interface BackgroundWorkerFgHostApi {
fun enableSyncWorker() fun enableSyncWorker()
fun enableUploadWorker(callbackHandle: Long) fun enableUploadWorker()
fun disableUploadWorker() fun disableUploadWorker()
companion object { companion object {
@ -93,11 +93,9 @@ interface BackgroundWorkerFgHostApi {
run { run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableUploadWorker$separatedMessageChannelSuffix", codec) val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableUploadWorker$separatedMessageChannelSuffix", codec)
if (api != null) { if (api != null) {
channel.setMessageHandler { message, reply -> channel.setMessageHandler { _, reply ->
val args = message as List<Any?>
val callbackHandleArg = args[0] as Long
val wrapped: List<Any?> = try { val wrapped: List<Any?> = try {
api.enableUploadWorker(callbackHandleArg) api.enableUploadWorker()
listOf(null) listOf(null)
} catch (exception: Throwable) { } catch (exception: Throwable) {
BackgroundWorkerPigeonUtils.wrapError(exception) BackgroundWorkerPigeonUtils.wrapError(exception)
@ -130,6 +128,7 @@ interface BackgroundWorkerFgHostApi {
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ /** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface BackgroundWorkerBgHostApi { interface BackgroundWorkerBgHostApi {
fun onInitialized() fun onInitialized()
fun close()
companion object { companion object {
/** The codec used by BackgroundWorkerBgHostApi. */ /** The codec used by BackgroundWorkerBgHostApi. */
@ -156,6 +155,22 @@ interface BackgroundWorkerBgHostApi {
channel.setMessageHandler(null) channel.setMessageHandler(null)
} }
} }
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.close$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
val wrapped: List<Any?> = try {
api.close()
listOf(null)
} catch (exception: Throwable) {
BackgroundWorkerPigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
} }
} }
} }

View file

@ -11,9 +11,8 @@ import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.SettableFuture import com.google.common.util.concurrent.SettableFuture
import io.flutter.FlutterInjector import io.flutter.FlutterInjector
import io.flutter.embedding.engine.FlutterEngine 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.embedding.engine.loader.FlutterLoader
import io.flutter.view.FlutterCallbackInformation
private const val TAG = "BackgroundWorker" private const val TAG = "BackgroundWorker"
@ -58,25 +57,6 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
loader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) { loader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) {
engine = FlutterEngine(ctx) 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 // Register custom plugins
MainActivity.registerPlugins(ctx, engine!!) MainActivity.registerPlugins(ctx, engine!!)
flutterApi = flutterApi =
@ -86,8 +66,12 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
api = this api = this
) )
engine!!.dartExecutor.executeDartCallback( engine!!.dartExecutor.executeDartEntrypoint(
DartCallback(ctx.assets, loader.findAppBundlePath(), callback) DartExecutor.DartEntrypoint(
loader.findAppBundlePath(),
"package:immich_mobile/domain/services/background_worker.service.dart",
"backgroundSyncNativeEntrypoint"
)
) )
} }
@ -109,14 +93,7 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
} }
} }
/** override fun close() {
* 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")
if (isComplete) { if (isComplete) {
return return
} }
@ -134,6 +111,16 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
}, 5000) }, 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<Unit>) { private fun handleHostResult(result: kotlin.Result<Unit>) {
if (isComplete) { if (isComplete) {
return return

View file

@ -21,9 +21,8 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
Log.i(TAG, "Scheduled media observer") Log.i(TAG, "Scheduled media observer")
} }
override fun enableUploadWorker(callbackHandle: Long) { override fun enableUploadWorker() {
updateUploadEnabled(ctx, true) updateUploadEnabled(ctx, true)
updateCallbackHandle(ctx, callbackHandle)
Log.i(TAG, "Scheduled background upload tasks") 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_NAME = "Immich::Background"
const val SHARED_PREF_BACKUP_ENABLED = "Background::backup::enabled" 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) { private fun updateUploadEnabled(context: Context, enabled: Boolean) {
context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE).edit { 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) { fun enqueueMediaObserver(ctx: Context) {
val constraints = Constraints.Builder() val constraints = Constraints.Builder()
.addContentUriTrigger(MediaStore.Images.Media.INTERNAL_CONTENT_URI, true) .addContentUriTrigger(MediaStore.Images.Media.INTERNAL_CONTENT_URI, true)

View file

@ -24,7 +24,7 @@ import UIKit
BackgroundServicePlugin.register(with: self.registrar(forPlugin: "BackgroundServicePlugin")!) BackgroundServicePlugin.register(with: self.registrar(forPlugin: "BackgroundServicePlugin")!)
BackgroundServicePlugin.registerBackgroundProcessing() BackgroundServicePlugin.registerBackgroundProcessing()
BackgroundWorkerApiImpl.registerBackgroundProcessing() BackgroundWorkerApiImpl.registerBackgroundWorkers()
BackgroundServicePlugin.setPluginRegistrantCallback { registry in BackgroundServicePlugin.setPluginRegistrantCallback { registry in
if !registry.hasPlugin("org.cocoapods.path-provider-foundation") { if !registry.hasPlugin("org.cocoapods.path-provider-foundation") {

View file

@ -74,7 +74,7 @@ class BackgroundWorkerPigeonCodec: FlutterStandardMessageCodec, @unchecked Senda
/// Generated protocol from Pigeon that represents a handler of messages from Flutter. /// Generated protocol from Pigeon that represents a handler of messages from Flutter.
protocol BackgroundWorkerFgHostApi { protocol BackgroundWorkerFgHostApi {
func enableSyncWorker() throws func enableSyncWorker() throws
func enableUploadWorker(callbackHandle: Int64) throws func enableUploadWorker() throws
func disableUploadWorker() 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) let enableUploadWorkerChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableUploadWorker\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api { if let api = api {
enableUploadWorkerChannel.setMessageHandler { message, reply in enableUploadWorkerChannel.setMessageHandler { _, reply in
let args = message as! [Any?]
let callbackHandleArg = args[0] as! Int64
do { do {
try api.enableUploadWorker(callbackHandle: callbackHandleArg) try api.enableUploadWorker()
reply(wrapResult(nil)) reply(wrapResult(nil))
} catch { } catch {
reply(wrapError(error)) reply(wrapError(error))
@ -130,6 +128,7 @@ class BackgroundWorkerFgHostApiSetup {
/// Generated protocol from Pigeon that represents a handler of messages from Flutter. /// Generated protocol from Pigeon that represents a handler of messages from Flutter.
protocol BackgroundWorkerBgHostApi { protocol BackgroundWorkerBgHostApi {
func onInitialized() throws func onInitialized() throws
func close() throws
} }
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
@ -151,6 +150,19 @@ class BackgroundWorkerBgHostApiSetup {
} else { } else {
onInitializedChannel.setMessageHandler(nil) 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. /// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift.

View file

@ -86,28 +86,10 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
* starts the engine, and sets up a timeout timer if specified. * starts the engine, and sets up a timeout timer if specified.
*/ */
func run() { 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 // Start the Flutter engine with the specified callback as the entry point
let isRunning = engine.run( let isRunning = engine.run(
withEntrypoint: callback.callbackName, withEntrypoint: "backgroundSyncNativeEntrypoint",
libraryURI: callback.callbackLibraryPath libraryURI: "package:immich_mobile/domain/services/background_worker.service.dart"
) )
// Verify that the Flutter engine started successfully // Verify that the Flutter engine started successfully
@ -127,7 +109,7 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
if maxSeconds != nil { if maxSeconds != nil {
// Schedule a timer to cancel the task after the specified timeout period // Schedule a timer to cancel the task after the specified timeout period
Timer.scheduledTimer(withTimeInterval: TimeInterval(maxSeconds!), repeats: false) { _ in 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 * 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. * the completion handler is eventually called even if Flutter doesn't respond.
*/ */
func cancel() { func close() {
if isComplete { if isComplete {
return return
} }
@ -182,7 +164,7 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
private func handleHostResult(result: Result<Void, PigeonError>) { private func handleHostResult(result: Result<Void, PigeonError>) {
switch result { switch result {
case .success(): self.complete(success: true) case .success(): self.complete(success: true)
case .failure(_): self.cancel() case .failure(_): self.close()
} }
} }

View file

@ -6,10 +6,8 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
print("BackgroundUploadImpl:enableSyncWorker Local Sync worker scheduled") print("BackgroundUploadImpl:enableSyncWorker Local Sync worker scheduled")
} }
func enableUploadWorker(callbackHandle: Int64) throws { func enableUploadWorker() throws {
BackgroundWorkerApiImpl.updateUploadEnabled(true) BackgroundWorkerApiImpl.updateUploadEnabled(true)
// Store the callback handle for later use when starting background Flutter isolates
BackgroundWorkerApiImpl.updateUploadCallbackHandle(callbackHandle)
BackgroundWorkerApiImpl.scheduleRefreshUpload() BackgroundWorkerApiImpl.scheduleRefreshUpload()
BackgroundWorkerApiImpl.scheduleProcessingUpload() BackgroundWorkerApiImpl.scheduleProcessingUpload()
@ -23,7 +21,6 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
} }
public static let backgroundUploadEnabledKey = "immich:background:backup:enabled" 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 localSyncTaskID = "app.alextran.immich.background.localSync"
private static let refreshUploadTaskID = "app.alextran.immich.background.refreshUpload" private static let refreshUploadTaskID = "app.alextran.immich.background.refreshUpload"
@ -33,17 +30,13 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
return UserDefaults.standard.set(isEnabled, forKey: BackgroundWorkerApiImpl.backgroundUploadEnabledKey) 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() { private static func cancelUploadTasks() {
BackgroundWorkerApiImpl.updateUploadEnabled(false) BackgroundWorkerApiImpl.updateUploadEnabled(false)
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: refreshUploadTaskID); BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: refreshUploadTaskID);
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: processingUploadTaskID); BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: processingUploadTaskID);
} }
public static func registerBackgroundProcessing() { public static func registerBackgroundWorkers() {
BGTaskScheduler.shared.register( BGTaskScheduler.shared.register(
forTaskWithIdentifier: processingUploadTaskID, using: nil) { task in forTaskWithIdentifier: processingUploadTaskID, using: nil) { task in
if task is BGProcessingTask { if task is BGProcessingTask {
@ -102,9 +95,22 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
} }
private static func handleBackgroundRefresh(task: BGAppRefreshTask, taskType: BackgroundTaskType) { private static func handleBackgroundRefresh(task: BGAppRefreshTask, taskType: BackgroundTaskType) {
scheduleRefreshUpload() let maxSeconds: Int?
// Restrict the refresh task to run only for a maximum of 20 seconds
runBackgroundWorker(task: task, taskType: taskType, maxSeconds: 20) 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) { private static func handleBackgroundProcessing(task: BGProcessingTask) {
@ -134,7 +140,7 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
task.expirationHandler = { task.expirationHandler = {
DispatchQueue.main.async { DispatchQueue.main.async {
backgroundWorker.cancel() backgroundWorker.close()
} }
isSuccess = false isSuccess = false

View file

@ -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/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/providers/user.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/app_settings.service.dart';
import 'package:immich_mobile/services/auth.service.dart'; import 'package:immich_mobile/services/auth.service.dart';
import 'package:immich_mobile/services/localization.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 // TODO: Move this call to native side once old timeline is removed
Future<void> enableSyncService() => _foregroundHostApi.enableSyncWorker(); Future<void> enableSyncService() => _foregroundHostApi.enableSyncWorker();
Future<void> enableUploadService() => _foregroundHostApi.enableUploadWorker( Future<void> enableUploadService() => _foregroundHostApi.enableUploadWorker();
PluginUtilities.getCallbackHandle(_backgroundSyncNativeEntrypoint)!.toRawHandle(),
);
Future<void> disableUploadService() => _foregroundHostApi.disableUploadWorker(); Future<void> disableUploadService() => _foregroundHostApi.disableUploadWorker();
} }
@ -44,7 +43,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
final Drift _drift; final Drift _drift;
final DriftLogger _driftLogger; final DriftLogger _driftLogger;
final BackgroundWorkerBgHostApi _backgroundHostApi; final BackgroundWorkerBgHostApi _backgroundHostApi;
final Logger _logger = Logger('BackgroundUploadBgService'); final Logger _logger = Logger('BackgroundWorkerBgService');
bool _isCleanedUp = false; bool _isCleanedUp = false;
@ -66,37 +65,50 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
bool get _isBackupEnabled => _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup); bool get _isBackupEnabled => _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup);
Future<void> init() async { Future<void> init() async {
await loadTranslations(); try {
HttpSSLOptions.apply(applyNative: false); await loadTranslations();
await _ref.read(authServiceProvider).setOpenApiServiceEndpoint(); HttpSSLOptions.apply(applyNative: false);
await _ref.read(authServiceProvider).setOpenApiServiceEndpoint();
// Initialize the file downloader // Initialize the file downloader
await FileDownloader().configure( await FileDownloader().configure(
globalConfig: [ globalConfig: [
// maxConcurrent: 6, maxConcurrentByHost(server):6, maxConcurrentByGroup: 3 // maxConcurrent: 6, maxConcurrentByHost(server):6, maxConcurrentByGroup: 3
(Config.holdingQueue, (6, 6, 3)), (Config.holdingQueue, (6, 6, 3)),
// On Android, if files are larger than 256MB, run in foreground service // On Android, if files are larger than 256MB, run in foreground service
(Config.runInForegroundIfFileLargerThan, 256), (Config.runInForegroundIfFileLargerThan, 256),
], ],
); );
await FileDownloader().trackTasksInGroup(kDownloadGroupLivePhoto, markDownloadedComplete: false); await FileDownloader().trackTasksInGroup(kDownloadGroupLivePhoto, markDownloadedComplete: false);
await FileDownloader().trackTasks(); await FileDownloader().trackTasks();
configureFileDownloaderNotifications(); configureFileDownloaderNotifications();
// Notify the host that the background upload service has been initialized and is ready to use await _ref.read(fileMediaRepositoryProvider).enableBackgroundAccess();
await _backgroundHostApi.onInitialized();
// 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 @override
Future<void> onLocalSync(int? maxSeconds) async { Future<void> onLocalSync(int? maxSeconds) async {
_logger.info('Local background syncing started'); try {
final sw = Stopwatch()..start(); _logger.info('Local background syncing started');
final sw = Stopwatch()..start();
final timeout = maxSeconds != null ? Duration(seconds: maxSeconds) : null; final timeout = maxSeconds != null ? Duration(seconds: maxSeconds) : null;
await _syncAssets(hashTimeout: timeout, syncRemote: false); await _syncAssets(hashTimeout: timeout, syncRemote: false);
sw.stop(); sw.stop();
_logger.info("Local sync completed in ${sw.elapsed.inSeconds}s"); _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 /* We do the following on Android upload
@ -107,16 +119,20 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
*/ */
@override @override
Future<void> onAndroidUpload() async { Future<void> onAndroidUpload() async {
_logger.info('Android background processing started'); try {
final sw = Stopwatch()..start(); _logger.info('Android background processing started');
final sw = Stopwatch()..start();
await _syncAssets(hashTimeout: Duration(minutes: _isBackupEnabled ? 3 : 6)); await _syncAssets(hashTimeout: Duration(minutes: _isBackupEnabled ? 3 : 6));
await _handleBackup(processBulk: false); await _handleBackup(processBulk: false);
await _cleanup(); sw.stop();
_logger.info("Android background processing completed in ${sw.elapsed.inSeconds}s");
sw.stop(); } catch (error, stack) {
_logger.info("Android background processing completed in ${sw.elapsed.inSeconds}s"); _logger.severe("Failed to complete Android background processing", error, stack);
} finally {
await _cleanup();
}
} }
/* We do the following on background upload /* We do the following on background upload
@ -129,29 +145,37 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
*/ */
@override @override
Future<void> onIosUpload(bool isRefresh, int? maxSeconds) async { Future<void> onIosUpload(bool isRefresh, int? maxSeconds) async {
_logger.info('iOS background upload started with maxSeconds: ${maxSeconds}s'); try {
final sw = Stopwatch()..start(); _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); final timeout = isRefresh ? const Duration(seconds: 5) : Duration(minutes: _isBackupEnabled ? 3 : 6);
await _syncAssets(hashTimeout: timeout); await _syncAssets(hashTimeout: timeout);
final backupFuture = _handleBackup(); final backupFuture = _handleBackup();
if (maxSeconds != null) { if (maxSeconds != null) {
await backupFuture.timeout(Duration(seconds: maxSeconds - 1), onTimeout: () {}); await backupFuture.timeout(Duration(seconds: maxSeconds - 1), onTimeout: () {});
} else { } else {
await backupFuture; 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 @override
Future<void> cancel() async { Future<void> cancel() async {
_logger.warning("Background upload cancelled"); _logger.warning("Background worker cancelled");
await _cleanup(); try {
await _cleanup();
} catch (error, stack) {
debugPrint('Failed to cleanup background worker: $error with stack: $stack');
}
} }
Future<void> _cleanup() async { Future<void> _cleanup() async {
@ -159,13 +183,21 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
return; return;
} }
_isCleanedUp = true; try {
await _ref.read(backgroundSyncProvider).cancel(); _isCleanedUp = true;
await _ref.read(backgroundSyncProvider).cancelLocal(); _logger.info("Cleaning up background worker");
await _isar.close(); await _ref.read(backgroundSyncProvider).cancel();
await _drift.close(); await _ref.read(backgroundSyncProvider).cancelLocal();
await _driftLogger.close(); if (_isar.isOpen) {
_ref.dispose(); 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<void> _handleBackup({bool processBulk = true}) async { Future<void> _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') @pragma('vm:entry-point')
Future<void> _backgroundSyncNativeEntrypoint() async { Future<void> backgroundSyncNativeEntrypoint() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
DartPluginRegistrant.ensureInitialized(); DartPluginRegistrant.ensureInitialized();

View file

@ -16,6 +16,13 @@ class StorageRepository {
file = await entity?.originFile; file = await entity?.originFile;
if (file == null) { if (file == null) {
log.warning("Cannot get file for asset $assetId"); 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) { } catch (error, stackTrace) {
log.warning("Error getting file for asset $assetId", error, stackTrace); log.warning("Error getting file for asset $assetId", error, stackTrace);
@ -34,6 +41,13 @@ class StorageRepository {
log.warning( log.warning(
"Cannot get motion file for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt}", "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) { } catch (error, stackTrace) {
log.warning( log.warning(

View file

@ -206,14 +206,14 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
// needs to be delayed so that EasyLocalization is working // needs to be delayed so that EasyLocalization is working
if (Store.isBetaTimelineEnabled) { if (Store.isBetaTimelineEnabled) {
ref.read(backgroundServiceProvider).disableService();
ref.read(driftBackgroundUploadFgService).enableSyncService(); ref.read(driftBackgroundUploadFgService).enableSyncService();
if (ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup)) { if (ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup)) {
ref.read(backgroundServiceProvider).disableService();
ref.read(driftBackgroundUploadFgService).enableUploadService(); ref.read(driftBackgroundUploadFgService).enableUploadService();
} }
} else { } else {
ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
ref.read(driftBackgroundUploadFgService).disableUploadService(); ref.read(driftBackgroundUploadFgService).disableUploadService();
ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
} }
}); });

View file

@ -82,7 +82,7 @@ class BackgroundWorkerFgHostApi {
} }
} }
Future<void> enableUploadWorker(int callbackHandle) async { Future<void> enableUploadWorker() async {
final String pigeonVar_channelName = final String pigeonVar_channelName =
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableUploadWorker$pigeonVar_messageChannelSuffix'; 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableUploadWorker$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>( final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
@ -90,7 +90,7 @@ class BackgroundWorkerFgHostApi {
pigeonChannelCodec, pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger, binaryMessenger: pigeonVar_binaryMessenger,
); );
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[callbackHandle]); final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?; final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) { if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName); throw _createConnectionError(pigeonVar_channelName);
@ -164,6 +164,29 @@ class BackgroundWorkerBgHostApi {
return; return;
} }
} }
Future<void> close() async {
final String pigeonVar_channelName =
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.close$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
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 { abstract class BackgroundWorkerFlutterApi {

View file

@ -15,8 +15,7 @@ import 'package:pigeon/pigeon.dart';
abstract class BackgroundWorkerFgHostApi { abstract class BackgroundWorkerFgHostApi {
void enableSyncWorker(); void enableSyncWorker();
// Enables the background upload service with the given callback handle void enableUploadWorker();
void enableUploadWorker(int callbackHandle);
// Disables the background upload service // Disables the background upload service
void disableUploadWorker(); void disableUploadWorker();
@ -27,6 +26,8 @@ abstract class BackgroundWorkerBgHostApi {
// Called from the background flutter engine when it has bootstrapped and established the // 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 // required platform channels to notify the native side to start the background upload
void onInitialized(); void onInitialized();
void close();
} }
@FlutterApi() @FlutterApi()