Skip to content
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

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

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from 2 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
84 changes: 84 additions & 0 deletions packages/firebase_messaging/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,90 @@ Note: When you are debugging on Android, use a device or AVD with Google Play se
</intent-filter>
```

#### Optionally handle background messages

>Background message handling is intended to be performed quickly. Do not perform
long running tasks as they may not be allowed to finish by the Android system.
See [Background Execution Limits](https://developer.android.com/about/versions/oreo/background)
for more.
By default background messaging is not enabled. To handle messages in the background:

1. For Android add an Application.java class to your app

```
package io.flutter.plugins.firebasemessagingexample;

import io.flutter.app.FlutterApplication;
import io.flutter.plugin.common.PluginRegistry;
import io.flutter.plugin.common.PluginRegistry.PluginRegistrantCallback;
import io.flutter.plugins.GeneratedPluginRegistrant;
import io.flutter.plugins.firebasemessaging.FlutterFirebaseMessagingService;

public class Application extends FlutterApplication implements PluginRegistrantCallback {
@Override
public void onCreate() {
super.onCreate();
FlutterFirebaseMessagingService.setPluginRegistrant(this);
}

@Override
public void registerWith(PluginRegistry registry) {
GeneratedPluginRegistrant.registerWith(registry);
}
}
```
1. Set name property of application in `AndroidManifest.xml`
```
<application android:name=".Application" ...>
```
1. For iOS (Swift)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where to add this code in ios? like android we create an Application class

```
func callback(registry: FlutterPluginRegistry) {
GeneratedPluginRegistrant.register(with: registry)
}

FLTFirebaseMessagingPlugin.setPluginRegistrantCallback(callback)
```

1. Define a top level Dart method to handle background messages
```
Future<dynamic> myBackgroundMessageHandler(Map<String, dynamic> message) {
if (message.containsKey('data')) {
// Handle data message
dynamic data = message['data'];
}

if (message.containsKey('notification')) {
// Handle notification message
dynamic notification = message['notification'];
}

// Or do other work.
}
```
Note: the protocol of `data` and `notification` are in line with the
fields defined by a [RemoteMessage](https://firebase.google.com/docs/reference/android/com/google/firebase/messaging/RemoteMessage).
1. Set `onBackgroundMessage` handler when calling `configure`
```
_firebaseMessaging.configure(
onMessage: (Map<String, dynamic> message) async {
print("onMessage: $message");
_showItemDialog(message);
},
onBackgroundMessage: myBackgroundMessageHandler,
onLaunch: (Map<String, dynamic> message) async {
print("onLaunch: $message");
_navigateToItemDetail(message);
},
onResume: (Map<String, dynamic> message) async {
print("onResume: $message");
_navigateToItemDetail(message);
},
);
```
Note: `configure` should be called early in the lifecycle of your application
so that it can be ready to receive messages as early as possible. See the
example app for a demonstration.

### iOS Integration

Expand Down
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
151 changes: 145 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_isolate" project:nil allowHeadlessExecution:YES];
_backgroundChannel = [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/firebase_messaging_background" binaryMessenger:[_headlessRunner binaryMessenger]];
}
return self;
}
Expand All @@ -75,6 +98,35 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result
[[UIApplication sharedApplication] registerUserNotificationSettings:settings];

result(nil);
/* Even when the app is not active the `FirebaseMessagingService` extended by
* `FlutterFirebaseMessagingService` allows incoming FCM messages to be handled.
*
* `FcmDartService#start` and `FcmDartService#initialized` are the two methods used
* to optionally setup handling messages received while the app is not active.
*
* `FcmDartService#start` sets up the plumbing that allows messages received while
* the app is not active to be handled by a background isolate.
*
* `FcmDartService#initialized` is called by the Dart side when the plumbing for
* background message handling is complete.
*/
} else if ([@"FcmDartService#start" isEqualToString:method]) {

} 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 +187,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 +231,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 +281,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
Loading