Skip to content

[firebase_messaging] Add support for handling messages in background for iOS #53

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions packages/firebase_messaging/example/ios/Runner/AppDelegate.m
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,19 @@

#include "AppDelegate.h"
#include "GeneratedPluginRegistrant.h"
#import <firebase_messaging/FirebaseMessagingPlugin.h>

@implementation AppDelegate

void callback(NSObject<FlutterPluginRegistry>* 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
5 changes: 5 additions & 0 deletions packages/firebase_messaging/example/ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>remote-notification</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
Expand Down
16 changes: 16 additions & 0 deletions packages/firebase_messaging/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<dynamic> myBackgroundMessageHandler(Map<String, dynamic> message) async {
print("run background message handler - $message");

return Future<void>.value();
}

final Map<String, Item> _items = <String, Item>{};

Item _itemForMessage(Map<String, dynamic> message) {
final dynamic data = message['data'] ?? message;
final String itemId = data['id'];
Expand All @@ -18,19 +26,24 @@ Item _itemForMessage(Map<String, dynamic> message) {

class Item {
Item({this.itemId});

final String itemId;

StreamController<Item> _controller = StreamController<Item>.broadcast();

Stream<Item> get onChanged => _controller.stream;

String _status;

String get status => _status;

set status(String value) {
_status = value;
_controller.add(this);
}

static final Map<String, Route<void>> routes = <String, Route<void>>{};

Route<void> get route {
final String routeName = '/detail/$itemId';
return routes.putIfAbsent(
Expand All @@ -45,7 +58,9 @@ class Item {

class DetailPage extends StatefulWidget {
DetailPage(this.itemId);

final String itemId;

@override
_DetailPageState createState() => _DetailPageState();
}
Expand Down Expand Up @@ -139,6 +154,7 @@ class _PushMessagingExampleState extends State<PushMessagingExample> {
void initState() {
super.initState();
_firebaseMessaging.configure(
onBackgroundMessage: myBackgroundMessageHandler,
onMessage: (Map<String, dynamic> message) async {
print("onMessage: $message");
_showItemDialog(message);
Expand Down
141 changes: 135 additions & 6 deletions packages/firebase_messaging/ios/Classes/FirebaseMessagingPlugin.m
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ @interface FLTFirebaseMessagingPlugin () <FIRMessagingDelegate>
@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]
Expand All @@ -21,16 +26,27 @@ @interface FLTFirebaseMessagingPlugin () <FIRMessagingDelegate>

@implementation FLTFirebaseMessagingPlugin {
FlutterMethodChannel *_channel;
FlutterMethodChannel *_backgroundChannel;
NSObject<FlutterPluginRegistrar> *_registrar;
NSUserDefaults *_userDefaults;
NSDictionary *_launchNotification;
NSMutableArray *_eventQueue;
BOOL _resumingFromBackground;
FlutterEngine *_headlessRunner;
BOOL initialized;
FetchCompletionHandler fetchCompletionHandler;
}

+ (void)setPluginRegistrantCallback:(FlutterPluginRegistrantCallback)callback {
registerPlugins = callback;
}

+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
FlutterMethodChannel *channel =
[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];

Expand All @@ -40,7 +56,7 @@ + (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
}
}

- (instancetype)initWithChannel:(FlutterMethodChannel *)channel {
- (instancetype)initWithChannel:(FlutterMethodChannel *)channel registrar:(NSObject<FlutterPluginRegistrar> *)registrar {
self = [super init];

if (self) {
Expand All @@ -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;
}
Expand All @@ -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];
Expand Down Expand Up @@ -135,6 +177,8 @@ - (void)applicationReceivedRemoteMessage:(FIRMessagingRemoteMessage *)remoteMess
#endif

- (void)didReceiveRemoteNotification:(NSDictionary *)userInfo {
NSLog(@"didReceiveRemoteNotification");

if (_resumingFromBackground) {
[_channel invokeMethod:@"onResume" arguments:userInfo];
} else {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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