From 9029ec5bb6719cb76ea41436b295ac3b66d4f11f Mon Sep 17 00:00:00 2001 From: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Date: Mon, 3 Nov 2025 23:51:21 +0530 Subject: [PATCH] fix: limit each android background run to 20 mins --- .../immich/background/BackgroundWorker.g.kt | 4 +- .../immich/background/BackgroundWorker.kt | 2 +- .../Background/BackgroundWorker.g.swift | 6 +-- .../services/background_worker.service.dart | 54 +++++++++++-------- .../lib/platform/background_worker_api.g.dart | 10 +++- mobile/pigeon/background_worker_api.dart | 2 +- 6 files changed, 46 insertions(+), 32 deletions(-) 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 b6b387db03..dde9cd57e1 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 @@ -295,12 +295,12 @@ class BackgroundWorkerFlutterApi(private val binaryMessenger: BinaryMessenger, p } } } - fun onAndroidUpload(callback: (Result) -> Unit) + fun onAndroidUpload(maxMinutesArg: Long?, callback: (Result) -> Unit) { val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" val channelName = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onAndroidUpload$separatedMessageChannelSuffix" val channel = BasicMessageChannel(binaryMessenger, channelName, codec) - channel.send(null) { + channel.send(listOf(maxMinutesArg)) { if (it is List<*>) { if (it.size > 1) { callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) 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 7dce1f6edf..716477904c 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 @@ -107,7 +107,7 @@ class BackgroundWorker(context: Context, params: WorkerParameters) : * This method acts as a bridge between the native Android background task system and Flutter. */ override fun onInitialized() { - flutterApi?.onAndroidUpload { handleHostResult(it) } + flutterApi?.onAndroidUpload(maxMinutesArg = 20) { handleHostResult(it) } } // TODO: Move this to a separate NotificationManager class diff --git a/mobile/ios/Runner/Background/BackgroundWorker.g.swift b/mobile/ios/Runner/Background/BackgroundWorker.g.swift index 8c9391e8d2..f9f412ebfa 100644 --- a/mobile/ios/Runner/Background/BackgroundWorker.g.swift +++ b/mobile/ios/Runner/Background/BackgroundWorker.g.swift @@ -295,7 +295,7 @@ class BackgroundWorkerBgHostApiSetup { /// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift. protocol BackgroundWorkerFlutterApiProtocol { func onIosUpload(isRefresh isRefreshArg: Bool, maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result) -> Void) - func onAndroidUpload(completion: @escaping (Result) -> Void) + func onAndroidUpload(maxMinutes maxMinutesArg: Int64?, completion: @escaping (Result) -> Void) func cancel(completion: @escaping (Result) -> Void) } class BackgroundWorkerFlutterApi: BackgroundWorkerFlutterApiProtocol { @@ -326,10 +326,10 @@ class BackgroundWorkerFlutterApi: BackgroundWorkerFlutterApiProtocol { } } } - func onAndroidUpload(completion: @escaping (Result) -> Void) { + func onAndroidUpload(maxMinutes maxMinutesArg: Int64?, completion: @escaping (Result) -> Void) { let channelName: String = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onAndroidUpload\(messageChannelSuffix)" let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) - channel.sendMessage(nil) { response in + channel.sendMessage([maxMinutesArg] as [Any?]) { response in guard let listResponse = response as? [Any?] else { completion(.failure(createConnectionError(withChannelName: channelName))) return diff --git a/mobile/lib/domain/services/background_worker.service.dart b/mobile/lib/domain/services/background_worker.service.dart index 5c228ba67c..4a714aa0b2 100644 --- a/mobile/lib/domain/services/background_worker.service.dart +++ b/mobile/lib/domain/services/background_worker.service.dart @@ -122,46 +122,54 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { } @override - Future onAndroidUpload() async { - _logger.info('Android background processing started'); - final sw = Stopwatch()..start(); - try { - if (!await _syncAssets(hashTimeout: Duration(minutes: _isBackupEnabled ? 3 : 6))) { - _logger.warning("Remote sync did not complete successfully, skipping backup"); - return; - } - await _handleBackup(); - } catch (error, stack) { - _logger.severe("Failed to complete Android background processing", error, stack); - } finally { - sw.stop(); - _logger.info("Android background processing completed in ${sw.elapsed.inSeconds}s"); - await _cleanup(); - } + Future onAndroidUpload(int? maxMinutes) async { + final hashTimeout = Duration(minutes: _isBackupEnabled ? 3 : 6); + final backupTimeout = maxMinutes != null ? Duration(minutes: maxMinutes - 1) : null; + return _backgroundLoop( + hashTimeout: hashTimeout, + backupTimeout: backupTimeout, + debugLabel: 'Android background upload', + ); } @override Future onIosUpload(bool isRefresh, int? maxSeconds) async { - _logger.info('iOS background upload started with maxSeconds: ${maxSeconds}s'); + final hashTimeout = isRefresh ? const Duration(seconds: 5) : Duration(minutes: _isBackupEnabled ? 3 : 6); + final backupTimeout = maxSeconds != null ? Duration(seconds: maxSeconds - 1) : null; + return _backgroundLoop(hashTimeout: hashTimeout, backupTimeout: backupTimeout, debugLabel: 'iOS background upload'); + } + + Future _backgroundLoop({ + required Duration hashTimeout, + required Duration? backupTimeout, + required String debugLabel, + }) async { + _logger.info( + '$debugLabel started hashTimeout: ${hashTimeout.inSeconds}s, backupTimeout: ${backupTimeout?.inMinutes ?? '~'}m', + ); final sw = Stopwatch()..start(); try { - final timeout = isRefresh ? const Duration(seconds: 5) : Duration(minutes: _isBackupEnabled ? 3 : 6); - if (!await _syncAssets(hashTimeout: timeout)) { + if (!await _syncAssets(hashTimeout: hashTimeout)) { _logger.warning("Remote sync did not complete successfully, skipping backup"); return; } final backupFuture = _handleBackup(); - if (maxSeconds != null) { - await backupFuture.timeout(Duration(seconds: maxSeconds - 1), onTimeout: () {}); + if (backupTimeout != null) { + await backupFuture.timeout( + backupTimeout, + onTimeout: () { + _cancellationToken.cancel(); + }, + ); } else { await backupFuture; } } catch (error, stack) { - _logger.severe("Failed to complete iOS background upload", error, stack); + _logger.severe("Failed to complete $debugLabel", error, stack); } finally { sw.stop(); - _logger.info("iOS background upload completed in ${sw.elapsed.inSeconds}s"); + _logger.info("$debugLabel completed in ${sw.elapsed.inSeconds}s"); await _cleanup(); } } diff --git a/mobile/lib/platform/background_worker_api.g.dart b/mobile/lib/platform/background_worker_api.g.dart index e8c87aa1a4..5a223a8e6c 100644 --- a/mobile/lib/platform/background_worker_api.g.dart +++ b/mobile/lib/platform/background_worker_api.g.dart @@ -273,7 +273,7 @@ abstract class BackgroundWorkerFlutterApi { Future onIosUpload(bool isRefresh, int? maxSeconds); - Future onAndroidUpload(); + Future onAndroidUpload(int? maxMinutes); Future cancel(); @@ -327,8 +327,14 @@ abstract class BackgroundWorkerFlutterApi { pigeonVar_channel.setMessageHandler(null); } else { pigeonVar_channel.setMessageHandler((Object? message) async { + assert( + message != null, + 'Argument for dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onAndroidUpload was null.', + ); + final List args = (message as List?)!; + final int? arg_maxMinutes = (args[0] as int?); try { - await api.onAndroidUpload(); + await api.onAndroidUpload(arg_maxMinutes); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); diff --git a/mobile/pigeon/background_worker_api.dart b/mobile/pigeon/background_worker_api.dart index a40d290199..06395fae7b 100644 --- a/mobile/pigeon/background_worker_api.dart +++ b/mobile/pigeon/background_worker_api.dart @@ -47,7 +47,7 @@ abstract class BackgroundWorkerFlutterApi { // Android Only: Called when the Android background upload is triggered @async - void onAndroidUpload(); + void onAndroidUpload(int? maxMinutes); @async void cancel();