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] Handle background messages on iOS #2016

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
8e841b3
added ios support
tobiasjunsten Dec 29, 2019
4d0844a
- renamed variable and removed commented log
tobiasjunsten Jan 8, 2020
3ca64a2
Merge branch 'master' of github.com:FirebaseExtended/flutterfire into…
tobiasjunsten Jan 8, 2020
08dcf2e
Merge branch 'master' of github.com:FirebaseExtended/flutterfire into…
tobiasjunsten Jan 22, 2020
44b68ba
Merge branch 'master' of github.com:FirebaseExtended/flutterfire into…
tobiasjunsten Feb 4, 2020
a87c807
Merge branch 'master' of github.com:FirebaseExtended/flutterfire into…
tobiasjunsten Feb 5, 2020
79e3fa8
Merge branch 'master' of github.com:FirebaseExtended/flutterfire into…
tobiasjunsten Feb 17, 2020
07fc453
[firebase_messaging] Added instructions for ios background handling i…
tobiasjunsten Feb 18, 2020
1824a2a
[firebase_messaging] Fixed some formatting
tobiasjunsten Feb 27, 2020
0225706
[firebase_messaging] Formatted the objective-c code
tobiasjunsten Feb 27, 2020
24cbd6f
Merge master into ios-background-support
tobiasjunsten Mar 27, 2020
34ad709
[firebase_messaging] Fixed readme after merge
tobiasjunsten Mar 27, 2020
afcb41b
[firebase_messaging] updated versions
tobiasjunsten Mar 27, 2020
7132d34
Merge branch 'master' into ios-background-support
tobiasjunsten Jun 17, 2020
566a977
[firebase_messaging] Added import firebase_messaging to the readme.
tobiasjunsten Jun 17, 2020
685f549
Merge branch 'master' into ios-background-support
tobiasjunsten Aug 18, 2020
9d495bf
[firebase_messaging] Updated readme with suggestions from @oskara
tobiasjunsten Aug 18, 2020
43deb43
Merge branch 'master' into ios-background-support
tobiasjunsten Sep 1, 2020
5b71c9a
- fixed spelling mistake in readme
tobiasjunsten Sep 1, 2020
e31f2cd
Merge branch 'master' into ios-background-support
tobiasjunsten Oct 23, 2020
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
4 changes: 4 additions & 0 deletions packages/firebase_messaging/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 7.0.4

* Added iOS support for background message handling.

## 7.0.3

- Update a dependency to the latest release.
Expand Down
105 changes: 62 additions & 43 deletions packages/firebase_messaging/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,46 @@ Note: When you are debugging on Android, use a device or AVD with Google Play se
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
```
#### Optionally handle background messages

### iOS Integration

To integrate your plugin into the iOS part of your app, follow these steps:

1. Generate the certificates required by Apple for receiving push notifications following [this guide](https://firebase.google.com/docs/cloud-messaging/ios/certs) in the Firebase docs. You can skip the section titled "Create the Provisioning Profile".

1. Using the [Firebase Console](https://console.firebase.google.com/) add an iOS app to your project: Follow the assistant, download the generated `GoogleService-Info.plist` file, open `ios/Runner.xcworkspace` with Xcode, and within Xcode place the file inside `ios/Runner`. **Don't** follow the steps named "Add Firebase SDK" and "Add initialization code" in the Firebase assistant.

1. In Xcode, select `Runner` in the Project Navigator. In the Capabilities Tab turn on `Push Notifications` and `Background Modes`, and enable `Background fetch` and `Remote notifications` under `Background Modes`.

1. Follow the steps in the "[Upload your APNs certificate](https://firebase.google.com/docs/cloud-messaging/ios/client#upload_your_apns_certificate)" section of the Firebase docs.

1. If you need to disable the method swizzling done by the FCM iOS SDK (e.g. so that you can use this plugin with other notification plugins) then add the following to your application's `Info.plist` file.

```xml
<key>FirebaseAppDelegateProxyEnabled</key>
<false/>
```

After that, add the following lines to the `(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions`
method in the `AppDelegate.m`/`AppDelegate.swift` of your iOS project.

Objective-C:
```objectivec
if (@available(iOS 10.0, *)) {
[UNUserNotificationCenter currentNotificationCenter].delegate = (id<UNUserNotificationCenterDelegate>) self;
}
```

Swift:
```swift
if #available(iOS 10.0, *) {
UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate
}
```

### Handle background messages (Optional)

#### Android configuration
>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)
Expand Down Expand Up @@ -110,6 +148,22 @@ By default background messaging is not enabled. To handle messages in the backgr
<application android:name=".Application" ...>
```

#### iOS configuration (Swift)
1. In the top of `AppDelegate.swift`, add the import of firebase_messaging:

```swift
import firebase_messaging
```

1. Then add the following code to `AppDelegate.swift` (E.g. last in the function `application` right before the return statement)):

```swift
FLTFirebaseMessagingPlugin.setPluginRegistrantCallback({ (registry: FlutterPluginRegistry) -> Void in
GeneratedPluginRegistrant.register(with: registry);
});
```

#### Usage in the common Dart code
1. Define a **TOP-LEVEL** or **STATIC** function to handle background messages

```dart
Expand Down Expand Up @@ -155,41 +209,6 @@ By default background messaging is not enabled. To handle messages in the backgr
so that it can be ready to receive messages as early as possible. See the
[example app](https://github.com/FirebaseExtended/flutterfire/tree/master/packages/firebase_messaging/example) for a demonstration.

### iOS Integration

To integrate your plugin into the iOS part of your app, follow these steps:

1. Generate the certificates required by Apple for receiving push notifications following [this guide](https://firebase.google.com/docs/cloud-messaging/ios/certs) in the Firebase docs. You can skip the section titled "Create the Provisioning Profile".

1. Using the [Firebase Console](https://console.firebase.google.com/) add an iOS app to your project: Follow the assistant, download the generated `GoogleService-Info.plist` file, open `ios/Runner.xcworkspace` with Xcode, and within Xcode place the file inside `ios/Runner`. **Don't** follow the steps named "Add Firebase SDK" and "Add initialization code" in the Firebase assistant.

1. In Xcode, select `Runner` in the Project Navigator. In the Capabilities Tab turn on `Push Notifications` and `Background Modes`, and enable `Background fetch` and `Remote notifications` under `Background Modes`.

1. Follow the steps in the "[Upload your APNs certificate](https://firebase.google.com/docs/cloud-messaging/ios/client#upload_your_apns_certificate)" section of the Firebase docs.

1. If you need to disable the method swizzling done by the FCM iOS SDK (e.g. so that you can use this plugin with other notification plugins) then add the following to your application's `Info.plist` file.

```xml
<key>FirebaseAppDelegateProxyEnabled</key>
<false/>
```

After that, add the following lines to the `(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions`
method in the `AppDelegate.m`/`AppDelegate.swift` of your iOS project.

Objective-C:
```objectivec
if (@available(iOS 10.0, *)) {
[UNUserNotificationCenter currentNotificationCenter].delegate = (id<UNUserNotificationCenterDelegate>) self;
}
```

Swift:
```swift
if #available(iOS 10.0, *) {
UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate
}
```

### Dart/Flutter Integration

Expand All @@ -205,14 +224,14 @@ Next, you should probably request permissions for receiving Push Notifications.

## Receiving Messages

Messages are sent to your Flutter app via the `onMessage`, `onLaunch`, and `onResume` callbacks that you configured with the plugin during setup. Here is how different message types are delivered on the supported platforms:
Messages are sent to your Flutter app via the `onMessage`, `onLaunch`, `onResume` and `onBackgroundMessage` callbacks that you configured with the plugin during setup. Here is how different message types are delivered on the supported platforms:

| | App in Foreground | App in Background | App Terminated |
| --------------------------: | ----------------- | ----------------- | -------------- |
| **Notification on Android** | `onMessage` | Notification is delivered to system tray. When the user clicks on it to open app `onResume` fires if `click_action: FLUTTER_NOTIFICATION_CLICK` is set (see below). | Notification is delivered to system tray. When the user clicks on it to open app `onLaunch` fires if `click_action: FLUTTER_NOTIFICATION_CLICK` is set (see below). |
| **Notification on iOS** | `onMessage` | Notification is delivered to system tray. When the user clicks on it to open app `onResume` fires. | Notification is delivered to system tray. When the user clicks on it to open app `onLaunch` fires. |
| **Data Message on Android** | `onMessage` | `onMessage` while app stays in the background. | *not supported by plugin, message is lost* |
| **Data Message on iOS** | `onMessage` | Message is stored by FCM and delivered to app via `onMessage` when the app is brought back to foreground. | Message is stored by FCM and delivered to app via `onMessage` when the app is brought back to foreground. |
| | App in Foreground | App in Background | App Terminated |
| --------------------------: | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Notification on Android** | `onMessage` | Notification is delivered to system tray. When the user clicks on it to open app `onResume` fires if `click_action: FLUTTER_NOTIFICATION_CLICK` is set (see below). | Notification is delivered to system tray. When the user clicks on it to open app `onLaunch` fires if `click_action: FLUTTER_NOTIFICATION_CLICK` is set (see below). |
| **Notification on iOS** | `onMessage` | Notification is delivered to system tray. When the user clicks on it to open app `onResume` fires. | Notification is delivered to system tray. When the user clicks on it to open app `onLaunch` fires. |
| **Data Message on Android** | `onMessage` | `onMessage` while app stays in the background. | *not supported by plugin, message is lost* |
| **Data Message on iOS** | `onMessage` | Message is delivered to `onBackgroundMessage`. This is the case both when app is running in background and the system has suspended the app. | When app is force-quit by the user the message is not handled. |

Additional reading: Firebase's [About FCM Messages](https://firebase.google.com/docs/cloud-messaging/concept-options).

Expand Down
161 changes: 157 additions & 4 deletions packages/firebase_messaging/ios/Classes/FLTFirebaseMessagingPlugin.m
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,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", (long)error.code]
Expand All @@ -26,17 +31,29 @@ @interface FLTFirebaseMessagingPlugin () <FIRMessagingDelegate>

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

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

+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
NSLog(@"registerWithRegistrar");
_registrar = 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 @@ -46,19 +63,32 @@ + (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
}
}

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

if (self) {
_channel = channel;
_resumingFromBackground = NO;
[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;
}

- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result {
NSString *method = call.method;
NSLog(@"handleMethodCall : %@", method);
if ([@"requestNotificationPermissions" isEqualToString:method]) {
NSDictionary *arguments = call.arguments;
if (@available(iOS 10.0, *)) {
Expand Down Expand Up @@ -132,6 +162,30 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result
[[UIApplication sharedApplication] registerForRemoteNotifications];
result([NSNumber numberWithBool:YES]);
}
} else if ([@"FcmDartService#start" isEqualToString:method]) {
NSDictionary *arguments = call.arguments;
NSLog(@"FcmDartService#start");
long setupHandle = [arguments[@"setupHandle"] longValue];
long backgroundHandle = [arguments[@"backgroundHandle"] longValue];
NSLog(@"FcmDartService#start with handle : %ld", setupHandle);
[self saveCallbackHandle:backgroundSetupCallback handle:setupHandle];
[self saveCallbackHandle:backgroundMessageCallback handle:backgroundHandle];
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 @@ -221,6 +275,7 @@ - (void)userNotificationCenter:(UNUserNotificationCenter *)center
#endif

- (void)didReceiveRemoteNotification:(NSDictionary *)userInfo {
NSLog(@"didReceiveRemoteNotification");
if (_resumingFromBackground) {
[_channel invokeMethod:@"onResume" arguments:userInfo];
} else {
Expand Down Expand Up @@ -252,8 +307,24 @@ - (void)applicationDidBecomeActive:(UIApplication *)application {
- (BOOL)application:(UIApplication *)application
didReceiveRemoteNotification:(NSDictionary *)userInfo
fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler {
[self didReceiveRemoteNotification:userInfo];
completionHandler(UIBackgroundFetchResultNoData);
NSLog(@"didReceiveRemoteNotification:completionHandler");
if (application.applicationState == UIApplicationStateBackground) {
// save this handler for later so it can be completed
fetchCompletionHandler = completionHandler;

[self queueMethodCall:@"handleBackgroundMessage"
callbackName:backgroundMessageCallback
arguments:userInfo];

if (!initialized) {
[self startBackgroundRunner];
}

} else {
[self didReceiveRemoteNotification:userInfo];
completionHandler(UIBackgroundFetchResultNewData);
}

return YES;
}

Expand Down Expand Up @@ -291,4 +362,86 @@ - (void)messaging:(FIRMessaging *)messaging
[_channel invokeMethod:@"onMessage" arguments:remoteMessage.appData];
}

- (void)setupBackgroundHandling:(int64_t)handle {
NSLog(@"Setting up Firebase background handling");

[self saveCallbackHandle:backgroundSetupCallback handle:handle];

NSLog(@"Finished background setup");
}

- (void)startBackgroundRunner {
NSLog(@"Starting background runner");

int64_t handle = [self getCallbackHandle:backgroundSetupCallback];

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);

NSDictionary *callbackArguments = @{
@"handle" : @(handle),
@"message" : arguments,
};

[_backgroundChannel invokeMethod:method
arguments:callbackArguments
result:^(id _Nullable result) {
NSLog(@"%@ method completed", method);
if (self->fetchCompletionHandler != nil) {
self->fetchCompletionHandler(UIBackgroundFetchResultNewData);
self->fetchCompletionHandler = nil;
}
}];
}

@end
2 changes: 1 addition & 1 deletion packages/firebase_messaging/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: firebase_messaging
description: Flutter plugin for Firebase Cloud Messaging, a cross-platform
messaging solution that lets you reliably deliver messages on Android and iOS.
homepage: https://github.com/FirebaseExtended/flutterfire/tree/master/packages/firebase_messaging
version: 7.0.3
version: 7.0.4

flutter:
plugin:
Expand Down