flutterfire icon indicating copy to clipboard operation
flutterfire copied to clipboard

πŸ› [firebase_messaging] There is trying to call the onBackgroundMessage callback too early.

Open sirkalmi opened this issue 1 year ago β€’ 2 comments

The problem is iOS specific. The problem occurs if the application was previously deleted from the memory. In this case, when the onBackgroundMessage callback is called in the didReceiveRemoteNotification method, it is still too early, because it was not yet possible to call the FirebaseMessaging.onBackgroundMessage method on the flutter side.

I solved the problem with a delay, but it's not a very nice solution. Can you fix the problem, or can you advise on a better solution?

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
          [_channel invokeMethod:@"Messaging#onBackgroundMessage"
                             arguments:notificationDict
                                result:^(id _Nullable result) {
                                  @synchronized(self) {
                                    if (completed == NO) {
                                      completed = YES;
                                      completionHandler(UIBackgroundFetchResultNewData);
                                      if (backgroundTaskId != UIBackgroundTaskInvalid) {
                                        [application endBackgroundTask:backgroundTaskId];
                                        backgroundTaskId = UIBackgroundTaskInvalid;
                                      }
                                    }
                                  }
                                }];
      });

sirkalmi avatar Sep 21 '22 07:09 sirkalmi

@sirkalmi Can you provide flutter doctor -v, a minimal code sample along with steps to replicate and logs that shows the behavior ?

darshankawar avatar Sep 21 '22 10:09 darshankawar

Output from flutter doctor:

[βœ“] Flutter (Channel stable, 3.3.2, on macOS 12.6 21G115 darwin-x64, locale hu-HU)
    β€’ Flutter version 3.3.2 on channel stable at /Users/sirkalmi/Programok/flutter
    β€’ Upstream repository https://github.com/flutter/flutter.git
    β€’ Framework revision e3c29ec00c (7 days ago), 2022-09-14 08:46:55 -0500
    β€’ Engine revision a4ff2c53d8
    β€’ Dart version 2.18.1
    β€’ DevTools version 2.15.0

[βœ“] Android toolchain - develop for Android devices (Android SDK version 30.0.2)
    β€’ Android SDK at /Users/sirkalmi/Programok/android-sdks/
    β€’ Platform android-33, build-tools 30.0.2
    β€’ Java binary at: /Applications/Android Studio.app/Contents/jre/Contents/Home/bin/java
    β€’ Java version OpenJDK Runtime Environment (build 11.0.12+0-b1504.28-7817840)
    β€’ All Android licenses accepted.

[βœ“] Xcode - develop for iOS and macOS (Xcode 14.0)
    β€’ Xcode at /Applications/Xcode.app/Contents/Developer
    β€’ Build 14A309
    β€’ CocoaPods version 1.11.3

[βœ“] Chrome - develop for the web
    β€’ Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[βœ“] Android Studio (version 2021.2)
    β€’ Android Studio at /Applications/Android Studio.app/Contents
    β€’ Flutter plugin can be installed from:
      πŸ”¨ https://plugins.jetbrains.com/plugin/9212-flutter
    β€’ Dart plugin can be installed from:
      πŸ”¨ https://plugins.jetbrains.com/plugin/6351-dart
    β€’ Java version OpenJDK Runtime Environment (build 11.0.12+0-b1504.28-7817840)

[βœ“] IntelliJ IDEA Ultimate Edition (version 2022.1)
    β€’ IntelliJ at /Applications/IntelliJ IDEA.app
    β€’ Flutter plugin can be installed from:
      πŸ”¨ https://plugins.jetbrains.com/plugin/9212-flutter
    β€’ Dart plugin can be installed from:
      πŸ”¨ https://plugins.jetbrains.com/plugin/6351-dart

[βœ“] IntelliJ IDEA Community Edition (version 2019.2.4)
    β€’ IntelliJ at /Applications/IntelliJ IDEA CE.app
    β€’ Flutter plugin can be installed from:
      πŸ”¨ https://plugins.jetbrains.com/plugin/9212-flutter
    β€’ Dart plugin can be installed from:
      πŸ”¨ https://plugins.jetbrains.com/plugin/6351-dart

[βœ“] Connected device (4 available)
    β€’ sdk gphone x86 (mobile)   β€’ emulator-5554                            β€’ android-x86    β€’ Android 11 (API 30) (emulator)
    β€’ Mobile iPhone-ja (mobile) β€’ 1d331ec4db8d495d84aa0e6afd2790d8ecc1555f β€’ ios            β€’ iOS 16.0 20A362
    β€’ macOS (desktop)           β€’ macos                                    β€’ darwin-x64     β€’ macOS 12.6 21G115 darwin-x64
    β€’ Chrome (web)              β€’ chrome                                   β€’ web-javascript β€’ Google Chrome 105.0.5195.125

[βœ“] HTTP Host Availability
    β€’ All required HTTP hosts are available

β€’ No issues found!
Logging output after deletion from memory when the first push arrives:
14:59:26.923310+0200	Runner	push_debug_native application:didFinishLaunchingWithOptions
14:59:27.037193+0200	Runner	push_debug_native didReceiveRemoteNotification:fetchCompletionHandler
14:59:27.080037+0200	Runner	flutter: 14:59:27 DEBUG push_debug_flutter _initFirebaseServices
14:59:27.080969+0200	Runner	flutter: 14:59:27 DEBUG push_debug_flutter initFirebaseMessaging

You can see that the didReceiveRemoteNotification method was run too early, before initFirebaseMessaging.

And when the second push arrives, when FirebaseMessaging has already been initialized:

15:38:29.089695+0200	Runner	push_debug_native application:didFinishLaunchingWithOptions
15:38:29.241386+0200	Runner	flutter: 15:38:29 DEBUG push_debug_flutter _initFirebaseServices
15:38:29.242434+0200	Runner	flutter: 15:38:29 DEBUG push_debug_flutter initFirebaseMessaging
15:38:30.223824+0200	Runner	push_debug_native didReceiveRemoteNotification:fetchCompletionHandler
15:38:30.226038+0200	Runner	flutter: 15:38:30 DEBUG push_debug_flutter _onBackgroundMessage

Everything is fine here, this is the correct run order.

Code snippet from flutter side:

void main() async {
  flavorConfig = FlavorConfig(...);
  run();
}

Future<void> run() async {
  WidgetsFlutterBinding.ensureInitialized();
  await initPackageInfo();
  await initStorageWrapper();
  await _initFlavorConfig();
  **await _initFirebaseServices();**
  await NotificationsController.initializeLocalNotifications();
  await _initSingletons();
  await _initWorker();
  await _initPreferredOrientations();
  await _initFlutterDownloader();
  _initGoogleMaps();
  runApp(await getApp());
}

Future<void> _initFirebaseServices() async {
  logger.d('push_debug _initFirebaseServices');
  await Firebase.initializeApp(options: F.firebaseOptions);
  ...
  await NotificationsController.initFirebaseMessaging();
}

static Future<void> initFirebaseMessaging() async {
    logger.d('push_debug initFirebaseMessaging');
    ...
    **FirebaseMessaging.onBackgroundMessage(_onBackgroundMessage);**
    FirebaseMessaging.onMessage.listen(_onMessage);
    ...
  }

  static Future<void> _onBackgroundMessage(RemoteMessage message) async {
    logger.d('push_debug _onBackgroundMessage');
    await handleNotification(message);
  }

Code snippet from iOS side:
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
    override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    	NSLog("parking_push application:didFinishLaunchingWithOptions");
        if let googleServiceFileName: String = Bundle.main.object(forInfoDictionaryKey: "GoogleService-Info") as! String? {
            if let filePath = Bundle.main.path(forResource: googleServiceFileName, ofType: "plist") {
                if let fileopts = FirebaseOptions(contentsOfFile: filePath) {
                    NSLog("Loading GoogleService-Info configuration...")
                    FirebaseApp.configure(options: fileopts)
                }
            }
        }
        
        if let googleApiKey: String = Bundle.main.object(forInfoDictionaryKey: "GOOGLE_API_KEY") as! String? {
            GMSServices.provideAPIKey(googleApiKey)
        }
        
        GeneratedPluginRegistrant.register(with: self)

        SwiftAwesomeNotificationsPlugin.setPluginRegistrantCallback { registry in
				  SwiftAwesomeNotificationsPlugin.register(
					with: registry.registrar(forPlugin: "io.flutter.plugins.awesomenotifications.AwesomeNotificationsPlugin")!)
				  FLTSharedPreferencesPlugin.register(
					with: registry.registrar(forPlugin: "io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin")!)
			  }
        
        FlutterDownloaderPlugin.setPluginRegistrantCallback({registry in
            if (!registry.hasPlugin("FlutterDownloaderPlugin")) {
                FlutterDownloaderPlugin.register(with: registry.registrar(forPlugin: "FlutterDownloaderPlugin")!)
            }
        })
        
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }

    override func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        super.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken)
        
        ...
        print("Received an APNs device token: \(readableToken)")
    }
    
    override func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
        super.application(application, didFailToRegisterForRemoteNotificationsWithError: error)
        
        print("PUSH registration failed: \(error)")
    }
}

sirkalmi avatar Sep 21 '22 11:09 sirkalmi

@sirkalmi Iam face same problem can you let me know where to put this code i want to try

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
          [_channel invokeMethod:@"Messaging#onBackgroundMessage"
                             arguments:notificationDict
                                result:^(id _Nullable result) {
                                  @synchronized(self) {
                                    if (completed == NO) {
                                      completed = YES;
                                      completionHandler(UIBackgroundFetchResultNewData);
                                      if (backgroundTaskId != UIBackgroundTaskInvalid) {
                                        [application endBackgroundTask:backgroundTaskId];
                                        backgroundTaskId = UIBackgroundTaskInvalid;
                                      }
                                    }
                                  }
                                }];
      });

Or if you can fork the package and add it will be great

hatemragab avatar Sep 21 '22 18:09 hatemragab

@sirkalmi thanks for the effort!

@hatemragab

  1. Fork Flutterfire

  2. Edit code here and push https://github.com/firebase/flutterfire/blob/master/packages/firebase_messaging/firebase_messaging/ios/Classes/FLTFirebaseMessagingPlugin.m

  3. Add your FirebaseMessaging fork to yalm: firebase_messaging: git: url: https://github.com/your_project/flutterfire.git ref: master path: packages/firebase_messaging/firebase_messaging

Unfortunately, I just tested it and it doesnt fix #9536. Code is a bit different but for us notifications work like before. @sirkalmi Does it fix 9536 for you? I mean, you kill the app (not just move it to background) and then open a different app. Do notifications work in that specific condition?

iosephmagno avatar Sep 21 '22 19:09 iosephmagno

Unfortunately, I just tested it and it doesnt fix #9536. Code is a bit different but for us notifications work like before. @sirkalmi Does it fix 9536 for you? I mean, you kill the app (not just move it to background) and then open a different app. Do notifications work in that specific condition?

I didn't know about bug #9536, it doesn't fix it. :-( I have other things to do today, but I'll debug #9536 as soon as I can.

sirkalmi avatar Sep 22 '22 05:09 sirkalmi

@hatemragab

Iam face same problem can you let me know where to put this code i want to try

  1. line: https://github.com/firebase/flutterfire/blob/master/packages/firebase_messaging/firebase_messaging/ios/Classes/FLTFirebaseMessagingPlugin.m

sirkalmi avatar Sep 22 '22 05:09 sirkalmi

Thanks for the updates. I am keeping this issue open for team's insight on expected behavior.

/cc @russellwheatley

darshankawar avatar Sep 22 '22 07:09 darshankawar

@sirkalmi Hello did you have a chance to debug it?

iosephmagno avatar Sep 26 '22 11:09 iosephmagno

@sirkalmi Hello did you have a chance to debug it?

No, because after clearing the application from memory, the connection to XCode's debugger is lost. Instead, I filled the code with logs, so it became visible where the processing gets stuck. I was clearly able to reproduce the bug and my fix works based on initial tester feedback.

sirkalmi avatar Sep 26 '22 13:09 sirkalmi

Did testers kill app or just move it to background? We just tested again on another iphone: if app is killed and another app is opened, then notifications dont work.

iosephmagno avatar Sep 26 '22 16:09 iosephmagno

Did testers kill app or just move it to background? We just tested again on another iphone: if app is killed and another app is opened, then notifications dont work.

They killed it but didn't open a new app. There seems to be a difference between just killing it or opening a new app afterwards.

sirkalmi avatar Sep 27 '22 06:09 sirkalmi

Yes, if you kill app, notifications still work untill you open a different app. This is why bug can go unnoticed at first glance

iosephmagno avatar Sep 27 '22 07:09 iosephmagno

Hello everyone, we’ve reviewed the issues opened for iOS background messages and we want to make our position clear.

Android and iOS handle background messaging differently. The decision made by the iOS operating system whether messages reaches the relevant iOS event handler (and subsequently received by the Dart background messaging handler) is based on a number of criteria, such as: CPU usage, priority level (data-only messages are considered β€œlow priority” by iOS, android does not), battery level, amount of messages being received by the app, background/terminated application state, etc. This is a fundamental difference between the platforms, and you need to be aware of them when designing your application.

One solution suggested for iOS data-only messages unreliability has been to add a NotificationExtension. But this will still not create parity with Android as you will still have to include a notification with your message (therefore not data-only). It will still render the original notification if you do not update/mutate the notification that comes through the system in a timely manner. We are not currently considering this as a solution due to these limitations.

We also will not support non-FCM messages from third party packages. We specifically only support messages received from the Firebase APIs since we cannot guarantee that messages received from third party packages will not have any unintended side-effects on other Firebase products such as messaging delivery reporting and Analytics data.

To help us triage and locate genuine issues that need to be addressed we have created a specific issue template for iOS background messages. If you believe you still have an issue that needs to be addressed, please create a new issue following this template.

I will be closing this issue in favor of raising a new issue with the new template above. This template will help you provide us with all the information we need to investigate a potential issue with background messaging on iOS.

russellwheatley avatar Sep 27 '22 11:09 russellwheatley