-
Notifications
You must be signed in to change notification settings - Fork 9.8k
[firebase_messaging] Add support for handling messages in background #1900
Conversation
Wouldn't it be better to have per default the same behaviour like on iOS ? The data message get's stored an onMessage is called at app start. We can then offer the possibility to override this behaviour and handle the message in background. Same thoughts for #1898 |
@The-Redhat Background message handling here is optional here. Storing the messages till the app is launched would be a departure from how the native SDKs generally operate. The desired functionality is for messages (non user visible) to be handled when they are received, otherwise the app could just perform required operations at next launch with no need for messaging. Here a background isolate is started (if enabled by the developer) to handle background messages as they arrive. I'd be happy to do this on iOS as well once I figure out background isolates for iOS. |
...ging/android/src/main/java/io/flutter/plugins/firebasemessaging/FirebaseMessagingPlugin.java
Outdated
Show resolved
Hide resolved
_channel.invokeMethod<bool>( | ||
'FcmDartService.start', | ||
<dynamic>[ | ||
backgroundSetupHandle.toRawHandle(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I think it's more readable/consistent to use named rather than positional arguments for MethodChannel
, unless there's only one argument.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Used a <String, dynamic> map so parameters can be named.
@@ -99,7 +110,28 @@ public void onReceive(Context context, Intent intent) { | |||
|
|||
@Override | |||
public void onMethodCall(final MethodCall call, final Result result) { | |||
if ("configure".equals(call.method)) { | |||
if ("FcmDartService.start".equals(call.method)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For consistency with other plugins that use method names to represent both a class and a method (e.g. storage), consider using #
here.
Also if the class you're calling is FlutterFirebaseMessagingService
would it make sense to put that here as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Used #
for consistency.
FlutterFIrebaseMessagingService
starts without the call from the Dart side. I'm happy to change the name but it should be something that indicates the setting up of the background channel.
...roid/src/main/java/io/flutter/plugins/firebasemessaging/FlutterFirebaseMessagingService.java
Show resolved
Hide resolved
new MethodChannel( | ||
registrar.messenger(), | ||
"plugins.flutter.io/android_fcm_background", | ||
JSONMethodCodec.INSTANCE); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just curious, why do we use JSONMethodCodec
here instead of the default?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is just a artifact from the alarm manager plugin I don't think it is necessary, the default codec should be sufficient.
import 'package:meta/meta.dart'; | ||
import 'package:platform/platform.dart'; | ||
|
||
typedef Future<dynamic> MessageHandler(Map<String, dynamic> message); | ||
|
||
const String _backgroundName = 'plugins.flutter.io/android_fcm_background'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure it makes sense to put this here as it's only used in one place, but should probably have channel in the method name
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
import 'package:meta/meta.dart'; | ||
import 'package:platform/platform.dart'; | ||
|
||
typedef Future<dynamic> MessageHandler(Map<String, dynamic> message); | ||
|
||
const String _backgroundName = 'plugins.flutter.io/android_fcm_background'; | ||
|
||
void _fcmSetupCallback() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You might want to put this code in the one place it's used, or in a static method of the class, it's unusual to have it in a function at the global scope.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added as a static member of FirebaseMessaging
const String _backgroundName = 'plugins.flutter.io/android_fcm_background'; | ||
|
||
void _fcmSetupCallback() { | ||
const MethodChannel _channel = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An underscore for hiding is confusing here since the variable is already scoped to the current function. I would either put this on the main class (so you can make it @visibleForTesting
) or just call it channel
here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This variable is no longer defined in this scope.
sBackgroundChannel.invokeMethod("", args, result); | ||
} | ||
|
||
// TODO(kroikie): Find a better way to determine application state. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@matthew-carroll might have ideas for other ways to do this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kroikie can you clarify what you mean technically by "foreground" in this case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@matthew-carroll the goal here is to determine when the Flutter process is still alive, in which case the original isolate can be used to process incoming messages. However when the Flutter process is no longer running the "background" isolate is used to process incoming messages.
Here we are checking if the app is in the foreground and visible, which is not exactly what the goal is but I could not find a clean way to determine if the Flutter process is still alive, this approximation was the closest I could get.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There should only be 1 process, and that's the app's process. If this code is executing then that process exists, by definition...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@matthew-carroll I'm not sure if this is actionable feedback for Arthur, are you ok with landing this as is?
``` | ||
<application android:name=".Application" ...> | ||
``` | ||
1. Define a top level method to handle background messages |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you explicitly mention that this is a Dart method so that readers do not get confused?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
1. Define a top level method to handle background messages | ||
``` | ||
Future<dynamic> myBackgroundMessageHandler(Map<String, dynamic> message) { | ||
if (message.containsKey('data')) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you mention somewhere where a developer can find the protocol that is being used here? The reference to 'data' and 'notification' looks magical.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done, it is in line with RemoteMessage fields.
dynamic data = message['data']; | ||
} | ||
|
||
if (message.containsKey('notification')) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can a message be both a "data" message and a "notification" message? If not, please consider using an if-else-if statement to make that explicit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RemoteMessages can contain both a data and notification part.
How the Android SDK passes the message over to the developer (and Plugin) depends on the state of the app.
} | ||
|
||
// Or do work with other plugins, eg: write to RTDB. | ||
FirebaseDatabase.instance.reference().child('foo').set('bar'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This reference to another plugin might be confusing for the reader - it seems to come out of nowhere. Also, are you sure this works as expected? You're executing in a background isolate, which will have a different version of the FirebaseDatabase plugin than the main isolate. This line might suggest to readers that there is only 1 FirebaseDatabase plugin and that they're controlling it from this background isolate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This example is to suggest that the developer is able to perform work in the background. In this case writing some data to RTDB. I assume the dev could use the shared preferences plugin to store some data locally.
I have tried this and it does work as expected from my tests, is there some reason why getting an instance of RTDB (which is a singleton) in a different isolate would be an issue?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the implementation of a plugin ensures that it shares resources across all plugin instances then things are fine. My concern here is that we're suggesting that you can interact with any plugin from here, but any plugin that does not setup its own singleton behavior would not work as expected...
...ging/android/src/main/java/io/flutter/plugins/firebasemessaging/FirebaseMessagingPlugin.java
Outdated
Show resolved
Hide resolved
...roid/src/main/java/io/flutter/plugins/firebasemessaging/FlutterFirebaseMessagingService.java
Show resolved
Hide resolved
|
||
public static void startBackgroundIsolate(Context context, long callbackHandle) { | ||
FlutterMain.ensureInitializationComplete(context, null); | ||
String mAppBundlePath = FlutterMain.findAppBundlePath(context); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the embedding I've migrated us away from hungarian notation. @collinjackson do you think we could migrate plugins away from it, too?
...roid/src/main/java/io/flutter/plugins/firebasemessaging/FlutterFirebaseMessagingService.java
Show resolved
Hide resolved
sBackgroundChannel.invokeMethod("", args, result); | ||
} | ||
|
||
// TODO(kroikie): Find a better way to determine application state. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kroikie can you clarify what you mean technically by "foreground" in this case?
CC @xster - This PR replicates a lot of the functionality from android_alarm_manager. I think you and I knew this was eventually coming after our conversation with @kroikie, but wanted to make sure its on your radar. We've got a very subpar solution for background execution in android_alarm_manager and we're seeing that expand here. We may want to prioritize a first-class background execution solution in the add-to-app timeline. |
@kroikie I have implemented the branch and maybe this might help developers who don't notice at first hand which I didn't. When creating the Application.java class. The package name needs to be specific to the application path of the file. If not the app breaks on launch.
I hope that helps. |
...roid/src/main/java/io/flutter/plugins/firebasemessaging/FlutterFirebaseMessagingService.java
Show resolved
Hide resolved
…lugins/firebasemessaging/FirebaseMessagingPlugin.java Co-Authored-By: Collin Jackson <jackson@google.com>
Responding late. We did talk about this and FWIW, doing a thing for now rather than building out some sort of dependency injection mechanism to keep the same engine instance is ok and pragmatic for now. Though it's "unsupported" and we can't guarantee what's going to happen when this creates 2 engine instances. |
Data messages when terminated will be supported soon. Check flutter/plugins#1900
...roid/src/main/java/io/flutter/plugins/firebasemessaging/FlutterFirebaseMessagingService.java
Outdated
Show resolved
Hide resolved
...ging/android/src/main/java/io/flutter/plugins/firebasemessaging/FirebaseMessagingPlugin.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm happy with landing this assuming @matthew-carroll is on board with the approach (for now). Just a few minor nits.
sBackgroundChannel.invokeMethod("", args, result); | ||
} | ||
|
||
// TODO(kroikie): Find a better way to determine application state. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@matthew-carroll I'm not sure if this is actionable feedback for Arthur, are you ok with landing this as is?
/// Your app should never call this method directly, this is only for use | ||
/// by the firebase_messaging plugin to setup background message handling. | ||
@visibleForTesting | ||
Future<void> fcmSetupBackgroundChannel( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I would lean towards making this a static void on FirebaseMessaging
and removing fcm
for stylistic consistency with other plugins. We don't usually namespace global functions with prefixes like this.
"background_message_callback"; | ||
|
||
// TODO(kroikie): make sIsIsolateRunning per-instance, not static. | ||
private static AtomicBoolean sIsIsolateRunning = new AtomicBoolean(false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Per @matthew-carroll's suggestion, let's migrate away from Hungarian notation in first-party plugins. I think his feedback from earlier in this PR is being hidden due to changes but here's a direct link.
I've moved this over to firebase/flutterfire#38 @kroikie, I've given you push access to my fork so you can push your changes there until it's ready to merge |
[firebase_messaging] Add Android support for handling messages in background
* commit 'bfc666daa89698c9fdc342e7ae521ec5e4e8ec5b': (59 commits) Fix analyze errors Make fcmSetupBackgroundChannel private Make fcmSetupBackgroundChannel top-level fix formatting fix analyze issues address comments Wrap chanel result logic in LatchResult Migration of flutter/plugins#1900 Remove emulators references from .cirrus.yaml format initial Update issue templates Update version/changelog per PR guidelines Remove duplicate example. reformat Update docs to encourage contributors to self-label PRs with the plugin they affect Manual merge of rejected diff Migrate PR Fix analyzer failures + reformat reformat ...
|
||
By default background messaging is not enabled. To handle messages in the background: | ||
|
||
1. Add an Application.java class to your app |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where in the App we should put this Java class, with the MainActivity or inside io.flutter.plugins?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@JillyTaboga You should add it alongside your MainActivity.java
file.
So, basically, this PR makes possible to handle background messages by following the info in the README ? Adding java class etc ? Would it be possible to get pointed to a complete example, so that maybe we can contribute to a documentation ? |
Description
When Flutter app is in the background or terminated allow it to handle incoming FCM messages. This update uses a similar strategy to the one used by the android_alarm_manager plugin. When the application starts an additional background channel is started to handle incoming messages when the app is not in the foreground.
Related Issues
flutter/flutter#32372
Related PR
#1898
Breaking Change
This is not a breaking change, but a version bump and change log entry will be needed.