diff --git a/packages/firebase_messaging/example/ios/Runner/AppDelegate.m b/packages/firebase_messaging/example/ios/Runner/AppDelegate.m index a4b51c88eb60..eec1108fdfe6 100644 --- a/packages/firebase_messaging/example/ios/Runner/AppDelegate.m +++ b/packages/firebase_messaging/example/ios/Runner/AppDelegate.m @@ -4,13 +4,19 @@ #include "AppDelegate.h" #include "GeneratedPluginRegistrant.h" +#import @implementation AppDelegate + +void callback(NSObject* registry) { + [GeneratedPluginRegistrant registerWithRegistry:registry]; +} - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [GeneratedPluginRegistrant registerWithRegistry:self]; - return [super application:application didFinishLaunchingWithOptions:launchOptions]; + [GeneratedPluginRegistrant registerWithRegistry:self]; + [FLTFirebaseMessagingPlugin setPluginRegistrantCallback:callback]; + return [super application:application didFinishLaunchingWithOptions:launchOptions]; } @end diff --git a/packages/firebase_messaging/example/ios/Runner/Info.plist b/packages/firebase_messaging/example/ios/Runner/Info.plist index ededae49efe7..1a05026fce18 100644 --- a/packages/firebase_messaging/example/ios/Runner/Info.plist +++ b/packages/firebase_messaging/example/ios/Runner/Info.plist @@ -24,6 +24,11 @@ 1 LSRequiresIPhoneOS + UIBackgroundModes + + fetch + remote-notification + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile diff --git a/packages/firebase_messaging/example/lib/main.dart b/packages/firebase_messaging/example/lib/main.dart index 685a3f86f2fa..ae30e141b546 100644 --- a/packages/firebase_messaging/example/lib/main.dart +++ b/packages/firebase_messaging/example/lib/main.dart @@ -7,7 +7,15 @@ import 'dart:async'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; +// myBackgroundMessageHandler must be either global or static otherwise you will encounter a runtime exception. +Future myBackgroundMessageHandler(Map message) async { + print("run background message handler - $message"); + + return Future.value(); +} + final Map _items = {}; + Item _itemForMessage(Map message) { final dynamic data = message['data'] ?? message; final String itemId = data['id']; @@ -18,19 +26,24 @@ Item _itemForMessage(Map message) { class Item { Item({this.itemId}); + final String itemId; StreamController _controller = StreamController.broadcast(); + Stream get onChanged => _controller.stream; String _status; + String get status => _status; + set status(String value) { _status = value; _controller.add(this); } static final Map> routes = >{}; + Route get route { final String routeName = '/detail/$itemId'; return routes.putIfAbsent( @@ -45,7 +58,9 @@ class Item { class DetailPage extends StatefulWidget { DetailPage(this.itemId); + final String itemId; + @override _DetailPageState createState() => _DetailPageState(); } @@ -139,6 +154,7 @@ class _PushMessagingExampleState extends State { void initState() { super.initState(); _firebaseMessaging.configure( + onBackgroundMessage: myBackgroundMessageHandler, onMessage: (Map message) async { print("onMessage: $message"); _showItemDialog(message); diff --git a/packages/firebase_messaging/ios/Classes/FirebaseMessagingPlugin.m b/packages/firebase_messaging/ios/Classes/FirebaseMessagingPlugin.m index 225b86f99599..89f34b8c3805 100644 --- a/packages/firebase_messaging/ios/Classes/FirebaseMessagingPlugin.m +++ b/packages/firebase_messaging/ios/Classes/FirebaseMessagingPlugin.m @@ -12,6 +12,11 @@ @interface FLTFirebaseMessagingPlugin () @end #endif +static NSString* backgroundSetupCallback = @"background_setup_callback"; +static NSString* backgroundMessageCallback = @"background_message_callback"; +static FlutterPluginRegistrantCallback registerPlugins = nil; +typedef void (^FetchCompletionHandler)(UIBackgroundFetchResult result); + static FlutterError *getFlutterError(NSError *error) { if (error == nil) return nil; return [FlutterError errorWithCode:[NSString stringWithFormat:@"Error %ld", error.code] @@ -21,8 +26,19 @@ @interface FLTFirebaseMessagingPlugin () @implementation FLTFirebaseMessagingPlugin { FlutterMethodChannel *_channel; + FlutterMethodChannel *_backgroundChannel; + NSObject *_registrar; + NSUserDefaults *_userDefaults; NSDictionary *_launchNotification; + NSMutableArray *_eventQueue; BOOL _resumingFromBackground; + FlutterEngine *_headlessRunner; + BOOL initialized; + FetchCompletionHandler fetchCompletionHandler; +} + ++ (void)setPluginRegistrantCallback:(FlutterPluginRegistrantCallback)callback { + registerPlugins = callback; } + (void)registerWithRegistrar:(NSObject *)registrar { @@ -30,7 +46,7 @@ + (void)registerWithRegistrar:(NSObject *)registrar { [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/firebase_messaging" binaryMessenger:[registrar messenger]]; FLTFirebaseMessagingPlugin *instance = - [[FLTFirebaseMessagingPlugin alloc] initWithChannel:channel]; + [[FLTFirebaseMessagingPlugin alloc] initWithChannel:channel registrar:registrar]; [registrar addApplicationDelegate:instance]; [registrar addMethodCallDelegate:instance channel:channel]; @@ -40,7 +56,7 @@ + (void)registerWithRegistrar:(NSObject *)registrar { } } -- (instancetype)initWithChannel:(FlutterMethodChannel *)channel { +- (instancetype)initWithChannel:(FlutterMethodChannel *)channel registrar:(NSObject *)registrar { self = [super init]; if (self) { @@ -52,6 +68,13 @@ - (instancetype)initWithChannel:(FlutterMethodChannel *)channel { NSLog(@"Configured the default Firebase app %@.", [FIRApp defaultApp].name); } [FIRMessaging messaging].delegate = self; + + // Setup background handling + _userDefaults = [NSUserDefaults standardUserDefaults]; + _eventQueue = [[NSMutableArray alloc] init]; + _registrar = registrar; + _headlessRunner = [[FlutterEngine alloc] initWithName:@"firebase_messaging_background" project:nil allowHeadlessExecution:YES]; + _backgroundChannel = [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/firebase_messaging_background" binaryMessenger:[_headlessRunner binaryMessenger]]; } return self; } @@ -75,6 +98,25 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result [[UIApplication sharedApplication] registerUserNotificationSettings:settings]; result(nil); + } else if ([@"FcmDartService#start" isEqualToString:method]) { + long handle = [call.arguments[0] longValue]; + [self saveCallbackHandle:backgroundMessageCallback handle:handle]; + result(nil); + } else if ([@"FcmDartService#initialized" isEqualToString:method]) { + /** + * Acknowledge that background message handling on the Dart side is ready. This is called by the + * Dart side once all background initialization is complete via `FcmDartService#initialized`. + */ + @synchronized(self) { + initialized = YES; + while ([_eventQueue count] > 0) { + NSArray* call = _eventQueue[0]; + [_eventQueue removeObjectAtIndex:0]; + + [self invokeMethod:call[0] callbackHandle:[call[1] longLongValue] arguments:call[2]]; + } + } + result(nil); } else if ([@"configure" isEqualToString:method]) { [FIRMessaging messaging].shouldEstablishDirectChannel = true; [[UIApplication sharedApplication] registerForRemoteNotifications]; @@ -135,6 +177,8 @@ - (void)applicationReceivedRemoteMessage:(FIRMessagingRemoteMessage *)remoteMess #endif - (void)didReceiveRemoteNotification:(NSDictionary *)userInfo { + NSLog(@"didReceiveRemoteNotification"); + if (_resumingFromBackground) { [_channel invokeMethod:@"onResume" arguments:userInfo]; } else { @@ -177,10 +221,23 @@ - (void)applicationDidBecomeActive:(UIApplication *)application { - (bool)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo - fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler { - [self didReceiveRemoteNotification:userInfo]; - completionHandler(UIBackgroundFetchResultNoData); - return YES; + fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler { + if (application.applicationState == UIApplicationStateBackground){ + //save this handler for later so it can be completed + fetchCompletionHandler = completionHandler; + + [self queueMethodCall:@"onMessageReceived" callbackName:backgroundMessageCallback arguments:userInfo]; + + if (!initialized){ + [self startBackgroundRunner]; + } + + } else { + [self didReceiveRemoteNotification:userInfo]; + completionHandler(UIBackgroundFetchResultNewData); + } + + return YES; } - (void)application:(UIApplication *)application @@ -214,4 +271,76 @@ - (void)messaging:(FIRMessaging *)messaging [_channel invokeMethod:@"onMessage" arguments:remoteMessage.appData]; } +- (void)setupBackgroundHandling:(int64_t)handle { + NSLog(@"Setting up Firebase background handling"); + + [self saveCallbackHandle:backgroundMessageCallback handle:handle]; + + NSLog(@"Finished background setup"); +} + +- (void)startBackgroundRunner { + NSLog(@"Starting background runner"); + + int64_t handle = [self getCallbackHandle:backgroundMessageCallback]; + + FlutterCallbackInformation *info = [FlutterCallbackCache lookupCallbackInformation:handle]; + NSAssert(info != nil, @"failed to find callback"); + NSString *entrypoint = info.callbackName; + NSString *uri = info.callbackLibraryPath; + + [_headlessRunner runWithEntrypoint:entrypoint libraryURI:uri]; + [_registrar addMethodCallDelegate:self channel:_backgroundChannel]; + + // Once our headless runner has been started, we need to register the application's plugins + // with the runner in order for them to work on the background isolate. `registerPlugins` is + // a callback set from AppDelegate.m in the main application. This callback should register + // all relevant plugins (excluding those which require UI). + + NSAssert(registerPlugins != nil, @"failed to set registerPlugins"); + registerPlugins(_headlessRunner); +} + +- (int64_t)getCallbackHandle:(NSString *) key { + NSLog(@"Getting callback handle for key %@", key); + id handle = [_userDefaults objectForKey:key]; + if (handle == nil) { + return 0; + } + return [handle longLongValue]; +} + +- (void) saveCallbackHandle:(NSString *)key handle:(int64_t)handle { + NSLog(@"Saving callback handle for key %@", key); + + [_userDefaults setObject:[NSNumber numberWithLongLong:handle] forKey:key]; +} + +- (void) queueMethodCall:(NSString *) method callbackName:(NSString*)callback arguments:(NSDictionary*)arguments { + NSLog(@"Queuing method call: %@", method); + int64_t handle = [self getCallbackHandle:callback]; + + @synchronized(self) { + if (initialized) { + [self invokeMethod:method callbackHandle:handle arguments:arguments]; + } else { + NSArray *call = @[method, @(handle), arguments]; + [_eventQueue addObject:call]; + } + } +} + +- (void) invokeMethod:(NSString *) method callbackHandle:(long)handle arguments:(NSDictionary*)arguments { + NSLog(@"Invoking method: %@", method); + NSArray* args = @[@(handle), arguments]; + + [_backgroundChannel invokeMethod:method arguments:args result:^(id _Nullable result) { + NSLog(@"%@ method completed", method); + if (self->fetchCompletionHandler!=nil) { + self->fetchCompletionHandler(UIBackgroundFetchResultNewData); + self->fetchCompletionHandler = nil; + } + }]; +} + @end