From 04d5fe13666b47ac16ccb742b0365695c6ea1cab Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Tue, 7 Oct 2025 19:19:43 +0530 Subject: [PATCH] fix: promote to foreground service before starting engine (#22517) fix: show notification from native Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Alex --- .../immich/background/BackgroundWorker.g.kt | 40 ++++++++-------- .../immich/background/BackgroundWorker.kt | 4 +- .../background/BackgroundWorkerApiImpl.kt | 4 ++ .../background/BackgroundWorkerPreferences.kt | 18 ++++++++ .../Background/BackgroundWorker.g.swift | 34 +++++++------- .../Runner/Background/BackgroundWorker.swift | 4 -- .../Background/BackgroundWorkerApiImpl.swift | 4 ++ .../services/background_worker.service.dart | 12 ++--- mobile/lib/main.dart | 10 ++++ .../lib/platform/background_worker_api.g.dart | 46 +++++++++---------- mobile/pigeon/background_worker_api.dart | 4 +- 11 files changed, 104 insertions(+), 76 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 052395c172..5857453ad3 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 @@ -136,6 +136,7 @@ private open class BackgroundWorkerPigeonCodec : StandardMessageCodec() { /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ interface BackgroundWorkerFgHostApi { fun enable() + fun saveNotificationMessage(title: String, body: String) fun configure(settings: BackgroundWorkerSettings) fun disable() @@ -164,6 +165,25 @@ interface BackgroundWorkerFgHostApi { channel.setMessageHandler(null) } } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.saveNotificationMessage$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val titleArg = args[0] as String + val bodyArg = args[1] as String + val wrapped: List = try { + api.saveNotificationMessage(titleArg, bodyArg) + listOf(null) + } catch (exception: Throwable) { + BackgroundWorkerPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } run { val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.configure$separatedMessageChannelSuffix", codec) if (api != null) { @@ -204,7 +224,6 @@ interface BackgroundWorkerFgHostApi { /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ interface BackgroundWorkerBgHostApi { fun onInitialized() - fun showNotification(title: String, content: String) fun close() companion object { @@ -232,25 +251,6 @@ interface BackgroundWorkerBgHostApi { channel.setMessageHandler(null) } } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.showNotification$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val titleArg = args[0] as String - val contentArg = args[1] as String - val wrapped: List = try { - api.showNotification(titleArg, contentArg) - listOf(null) - } catch (exception: Throwable) { - BackgroundWorkerPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } run { val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.close$separatedMessageChannelSuffix", codec) if (api != 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 71d9f5ffe3..e59cee2c16 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 @@ -73,6 +73,8 @@ class BackgroundWorker(context: Context, params: WorkerParameters) : NotificationManager.IMPORTANCE_LOW ) notificationManager.createNotificationChannel(notificationChannel) + val notificationConfig = BackgroundWorkerPreferences(ctx).getNotificationConfig() + showNotification(notificationConfig.first, notificationConfig.second) loader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) { engine = FlutterEngine(ctx) @@ -109,7 +111,7 @@ class BackgroundWorker(context: Context, params: WorkerParameters) : } // TODO: Move this to a separate NotificationManager class - override fun showNotification(title: String, content: String) { + private fun showNotification(title: String, content: String) { val notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ID) .setSmallIcon(R.drawable.notification_icon) .setOnlyAlertOnce(true) 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 78f2e9e461..a78db3c5ea 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 @@ -20,6 +20,10 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi { enqueueMediaObserver(ctx) } + override fun saveNotificationMessage(title: String, body: String) { + BackgroundWorkerPreferences(ctx).updateNotificationConfig(title, body) + } + override fun configure(settings: BackgroundWorkerSettings) { BackgroundWorkerPreferences(ctx).updateSettings(settings) enqueueMediaObserver(ctx) diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorkerPreferences.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorkerPreferences.kt index cfceb06c1d..450113e5b0 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorkerPreferences.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorkerPreferences.kt @@ -10,9 +10,13 @@ class BackgroundWorkerPreferences(private val ctx: Context) { private const val SHARED_PREF_MIN_DELAY_KEY = "BackgroundWorker::minDelaySeconds" private const val SHARED_PREF_REQUIRE_CHARGING_KEY = "BackgroundWorker::requireCharging" private const val SHARED_PREF_LOCK_KEY = "BackgroundWorker::isLocked" + private const val SHARED_PREF_NOTIF_TITLE_KEY = "BackgroundWorker::notificationTitle" + private const val SHARED_PREF_NOTIF_MSG_KEY = "BackgroundWorker::notificationMessage" private const val DEFAULT_MIN_DELAY_SECONDS = 30L private const val DEFAULT_REQUIRE_CHARGING = false + private const val DEFAULT_NOTIF_TITLE = "Uploading media" + private const val DEFAULT_NOTIF_MSG = "Checking for new assets…" } private val sp: SharedPreferences by lazy { @@ -38,6 +42,20 @@ class BackgroundWorkerPreferences(private val ctx: Context) { ) } + fun updateNotificationConfig(title: String, message: String) { + sp.edit { + putString(SHARED_PREF_NOTIF_TITLE_KEY, title) + putString(SHARED_PREF_NOTIF_MSG_KEY, message) + } + } + + fun getNotificationConfig(): Pair { + val title = + sp.getString(SHARED_PREF_NOTIF_TITLE_KEY, DEFAULT_NOTIF_TITLE) ?: DEFAULT_NOTIF_TITLE + val message = sp.getString(SHARED_PREF_NOTIF_MSG_KEY, DEFAULT_NOTIF_MSG) ?: DEFAULT_NOTIF_MSG + return Pair(title, message) + } + fun setLocked(paused: Boolean) { sp.edit { putBoolean(SHARED_PREF_LOCK_KEY, paused) diff --git a/mobile/ios/Runner/Background/BackgroundWorker.g.swift b/mobile/ios/Runner/Background/BackgroundWorker.g.swift index ece5cd5f64..e339f150e7 100644 --- a/mobile/ios/Runner/Background/BackgroundWorker.g.swift +++ b/mobile/ios/Runner/Background/BackgroundWorker.g.swift @@ -182,6 +182,7 @@ class BackgroundWorkerPigeonCodec: FlutterStandardMessageCodec, @unchecked Senda /// Generated protocol from Pigeon that represents a handler of messages from Flutter. protocol BackgroundWorkerFgHostApi { func enable() throws + func saveNotificationMessage(title: String, body: String) throws func configure(settings: BackgroundWorkerSettings) throws func disable() throws } @@ -205,6 +206,22 @@ class BackgroundWorkerFgHostApiSetup { } else { enableChannel.setMessageHandler(nil) } + let saveNotificationMessageChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.saveNotificationMessage\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + saveNotificationMessageChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let titleArg = args[0] as! String + let bodyArg = args[1] as! String + do { + try api.saveNotificationMessage(title: titleArg, body: bodyArg) + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + saveNotificationMessageChannel.setMessageHandler(nil) + } let configureChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.configure\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { configureChannel.setMessageHandler { message, reply in @@ -238,7 +255,6 @@ class BackgroundWorkerFgHostApiSetup { /// Generated protocol from Pigeon that represents a handler of messages from Flutter. protocol BackgroundWorkerBgHostApi { func onInitialized() throws - func showNotification(title: String, content: String) throws func close() throws } @@ -261,22 +277,6 @@ class BackgroundWorkerBgHostApiSetup { } else { onInitializedChannel.setMessageHandler(nil) } - let showNotificationChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.showNotification\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - showNotificationChannel.setMessageHandler { message, reply in - let args = message as! [Any?] - let titleArg = args[0] as! String - let contentArg = args[1] as! String - do { - try api.showNotification(title: titleArg, content: contentArg) - reply(wrapResult(nil)) - } catch { - reply(wrapError(error)) - } - } - } else { - showNotificationChannel.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 diff --git a/mobile/ios/Runner/Background/BackgroundWorker.swift b/mobile/ios/Runner/Background/BackgroundWorker.swift index c3268b4a2b..15df971203 100644 --- a/mobile/ios/Runner/Background/BackgroundWorker.swift +++ b/mobile/ios/Runner/Background/BackgroundWorker.swift @@ -119,10 +119,6 @@ class BackgroundWorker: BackgroundWorkerBgHostApi { }) } - func showNotification(title: String, content: String) throws { - // No-op on iOS for the time being - } - /** * Cancels the currently running background task, either due to timeout or external request. * Sends a cancel signal to the Flutter side and sets up a fallback timer to ensure diff --git a/mobile/ios/Runner/Background/BackgroundWorkerApiImpl.swift b/mobile/ios/Runner/Background/BackgroundWorkerApiImpl.swift index f7f8f69989..a7bbc31ceb 100644 --- a/mobile/ios/Runner/Background/BackgroundWorkerApiImpl.swift +++ b/mobile/ios/Runner/Background/BackgroundWorkerApiImpl.swift @@ -12,6 +12,10 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi { // Android only } + func saveNotificationMessage(title: String, body: String) throws { + // Android only + } + func disable() throws { BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: BackgroundWorkerApiImpl.refreshTaskID); BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: BackgroundWorkerApiImpl.processingTaskID); diff --git a/mobile/lib/domain/services/background_worker.service.dart b/mobile/lib/domain/services/background_worker.service.dart index 0548a45bf7..d95b1d4951 100644 --- a/mobile/lib/domain/services/background_worker.service.dart +++ b/mobile/lib/domain/services/background_worker.service.dart @@ -11,8 +11,6 @@ import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/network_capability_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/generated/intl_keys.g.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart'; import 'package:immich_mobile/platform/background_worker_api.g.dart'; @@ -44,6 +42,9 @@ class BackgroundWorkerFgService { // TODO: Move this call to native side once old timeline is removed Future enable() => _foregroundHostApi.enable(); + Future saveNotificationMessage(String title, String body) => + _foregroundHostApi.saveNotificationMessage(title, body); + Future configure({int? minimumDelaySeconds, bool? requireCharging}) => _foregroundHostApi.configure( BackgroundWorkerSettings( minimumDelaySeconds: @@ -112,13 +113,6 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { configureFileDownloaderNotifications(); - if (Platform.isAndroid) { - await _backgroundHostApi.showNotification( - IntlKeys.uploading_media.t(), - IntlKeys.backup_background_service_default_notification.t(), - ); - } - // Notify the host that the background worker service has been initialized and is ready to use _backgroundHostApi.onInitialized(); } catch (error, stack) { diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 712ee0bd83..263a5ef769 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -15,7 +15,9 @@ import 'package:immich_mobile/constants/locales.dart'; import 'package:immich_mobile/domain/services/background_worker.service.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/generated/codegen_loader.g.dart'; +import 'package:immich_mobile/generated/intl_keys.g.dart'; import 'package:immich_mobile/platform/background_worker_lock_api.g.dart'; import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart'; @@ -210,6 +212,14 @@ class ImmichAppState extends ConsumerState with WidgetsBindingObserve if (Store.isBetaTimelineEnabled) { ref.read(backgroundServiceProvider).disableService(); ref.read(backgroundWorkerFgServiceProvider).enable(); + if (Platform.isAndroid) { + ref + .read(backgroundWorkerFgServiceProvider) + .saveNotificationMessage( + IntlKeys.uploading_media.t(), + IntlKeys.backup_background_service_default_notification.t(), + ); + } } else { ref.read(backgroundWorkerFgServiceProvider).disable(); 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 af7c78fd4b..22325603c0 100644 --- a/mobile/lib/platform/background_worker_api.g.dart +++ b/mobile/lib/platform/background_worker_api.g.dart @@ -138,6 +138,29 @@ class BackgroundWorkerFgHostApi { } } + Future saveNotificationMessage(String title, String body) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.saveNotificationMessage$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([title, body]); + 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; + } + } + Future configure(BackgroundWorkerSettings settings) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.configure$pigeonVar_messageChannelSuffix'; @@ -221,29 +244,6 @@ class BackgroundWorkerBgHostApi { } } - Future showNotification(String title, String content) async { - final String pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.showNotification$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([title, content]); - 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; - } - } - Future close() async { final String pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.close$pigeonVar_messageChannelSuffix'; diff --git a/mobile/pigeon/background_worker_api.dart b/mobile/pigeon/background_worker_api.dart index 6f6c781de2..a40d290199 100644 --- a/mobile/pigeon/background_worker_api.dart +++ b/mobile/pigeon/background_worker_api.dart @@ -22,6 +22,8 @@ class BackgroundWorkerSettings { abstract class BackgroundWorkerFgHostApi { void enable(); + void saveNotificationMessage(String title, String body); + void configure(BackgroundWorkerSettings settings); void disable(); @@ -33,8 +35,6 @@ abstract class BackgroundWorkerBgHostApi { // required platform channels to notify the native side to start the background upload void onInitialized(); - void showNotification(String title, String content); - // Called from the background flutter engine to request the native side to cleanup void close(); }