Skip to content

Commit

Permalink
firebase#47 - initial implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
Genert Org committed Aug 28, 2019
1 parent cdbcf01 commit 72f0b46
Show file tree
Hide file tree
Showing 4 changed files with 215 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,15 @@
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"idiom" : "ios-marketing",
"size" : "1024x1024",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
}
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
61 changes: 59 additions & 2 deletions packages/firebase_messaging/lib/firebase_messaging.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
// found in the LICENSE file.

import 'dart:async';
import 'dart:io';
import 'dart:ui';

import 'package:flutter/services.dart';
import 'package:meta/meta.dart';
import 'package:platform/platform.dart';
import 'package:flutter/widgets.dart';

typedef Future<dynamic> MessageHandler(Map<String, dynamic> message);

Expand All @@ -26,10 +29,49 @@ class FirebaseMessaging {
const MethodChannel('plugins.flutter.io/firebase_messaging'),
const LocalPlatform());

/// Setup method channel to handle Firebase Cloud Messages received while
/// the Flutter app is not active. The handle for this method is generated
/// and passed to the Android side so that the background isolate knows where
/// to send background messages for processing.
///
/// Your app should never call this method directly, this is only for use
/// by the firebase_messaging plugin to setup background message handling.
@visibleForTesting
static void fcmSetupBackgroundChannel(
{MethodChannel backgroundChannel = const MethodChannel(
'plugins.flutter.io/firebase_messaging_background')}) async {
// Setup Flutter state needed for MethodChannels.
WidgetsFlutterBinding.ensureInitialized();

// This is where the magic happens and we handle background events from the
// native portion of the plugin.
backgroundChannel.setMethodCallHandler((MethodCall call) async {
if (call.method == 'handleBackgroundMessage') {
final CallbackHandle handle =
CallbackHandle.fromRawHandle(call.arguments['handle']);
final Function handlerFunction =
PluginUtilities.getCallbackFromHandle(handle);
try {
await handlerFunction(
Map<String, dynamic>.from(call.arguments['message']));
} catch (e) {
print('Unable to handle incoming background message.');
print(e);
}
return Future<void>.value();
}
});

// Once we've finished initializing, let the native portion of the plugin
// know that it can start scheduling handling messages.
backgroundChannel.invokeMethod<void>('FcmDartService#initialized');
}

final MethodChannel _channel;
final Platform _platform;

MessageHandler _onMessage;
MessageHandler _onBackgroundMessage;
MessageHandler _onLaunch;
MessageHandler _onResume;

Expand All @@ -47,7 +89,7 @@ class FirebaseMessaging {
}

final StreamController<IosNotificationSettings> _iosSettingsStreamController =
StreamController<IosNotificationSettings>.broadcast();
StreamController<IosNotificationSettings>.broadcast();

/// Stream that fires when the user changes their notification settings.
///
Expand All @@ -59,6 +101,7 @@ class FirebaseMessaging {
/// Sets up [MessageHandler] for incoming messages.
void configure({
MessageHandler onMessage,
MessageHandler onBackgroundMessage,
MessageHandler onLaunch,
MessageHandler onResume,
}) {
Expand All @@ -67,10 +110,24 @@ class FirebaseMessaging {
_onResume = onResume;
_channel.setMethodCallHandler(_handleMethod);
_channel.invokeMethod<void>('configure');
if (onBackgroundMessage != null) {
_onBackgroundMessage = onBackgroundMessage;
final CallbackHandle backgroundSetupHandle =
PluginUtilities.getCallbackHandle(fcmSetupBackgroundChannel);
final CallbackHandle backgroundMessageHandle =
PluginUtilities.getCallbackHandle(_onBackgroundMessage);
_channel.invokeMethod<bool>(
'FcmDartService#start',
<String, dynamic>{
'setupHandle': backgroundSetupHandle.toRawHandle(),
'backgroundHandle': backgroundMessageHandle.toRawHandle()
},
);
}
}

final StreamController<String> _tokenStreamController =
StreamController<String>.broadcast();
StreamController<String>.broadcast();

/// Fires when a new FCM token is generated.
Stream<String> get onTokenRefresh {
Expand Down

0 comments on commit 72f0b46

Please sign in to comment.