Skip to content
This repository has been archived by the owner on Feb 22, 2023. It is now read-only.

[firebase_messaging] Add support for handling messages in background #1900

Closed
wants to merge 16 commits into from

Conversation

kroikie
Copy link
Contributor

@kroikie kroikie commented Jul 24, 2019

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.

@collinjackson
Copy link
Contributor

Thanks. @ivk1800 I'd love to get your thoughts on how this PR compares to #1898 since it seems like they're both tackling the same problem.

@The-Redhat
Copy link
Contributor

The-Redhat commented Jul 25, 2019

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

@kroikie
Copy link
Contributor Author

kroikie commented Jul 25, 2019

@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.

@kroikie kroikie changed the title [WIP][firebase_messaging]Add support for handling messages in background [firebase_messaging]Add support for handling messages in background Jul 30, 2019
@kroikie
Copy link
Contributor Author

kroikie commented Jul 30, 2019

@ivk1800 If you can please take a look to ensure that this PR covers the use case you solved in #1898

@kroikie kroikie changed the title [firebase_messaging]Add support for handling messages in background [firebase_messaging] Add support for handling messages in background Jul 31, 2019
_channel.invokeMethod<bool>(
'FcmDartService.start',
<dynamic>[
backgroundSetupHandle.toRawHandle(),
Copy link
Contributor

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.

Copy link
Contributor Author

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.

packages/firebase_messaging/README.md Show resolved Hide resolved
@@ -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)) {
Copy link
Contributor

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?

Copy link
Contributor Author

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.

new MethodChannel(
registrar.messenger(),
"plugins.flutter.io/android_fcm_background",
JSONMethodCodec.INSTANCE);
Copy link
Contributor

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?

Copy link
Contributor Author

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';
Copy link
Contributor

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

Copy link
Contributor Author

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() {
Copy link
Contributor

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.

Copy link
Contributor Author

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 =
Copy link
Contributor

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.

Copy link
Contributor Author

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.
Copy link
Contributor

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

Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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...

Copy link
Contributor

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
Copy link
Contributor

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?

Copy link
Contributor Author

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')) {
Copy link
Contributor

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.

Copy link
Contributor Author

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')) {
Copy link
Contributor

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.

Copy link
Contributor Author

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');
Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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...

packages/firebase_messaging/README.md Show resolved Hide resolved

public static void startBackgroundIsolate(Context context, long callbackHandle) {
FlutterMain.ensureInitializationComplete(context, null);
String mAppBundlePath = FlutterMain.findAppBundlePath(context);
Copy link
Contributor

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?

sBackgroundChannel.invokeMethod("", args, result);
}

// TODO(kroikie): Find a better way to determine application state.
Copy link
Contributor

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?

@matthew-carroll
Copy link
Contributor

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.

@XExistence
Copy link

@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.

package "your specific package name of your app"

I hope that helps.

…lugins/firebasemessaging/FirebaseMessagingPlugin.java

Co-Authored-By: Collin Jackson <jackson@google.com>
@xster
Copy link
Member

xster commented Aug 13, 2019

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.

Levi-Lesches added a commit to Student-Lyf/RamLife that referenced this pull request Aug 14, 2019
Data messages when terminated will be supported soon.
Check flutter/plugins#1900
Copy link
Contributor

@collinjackson collinjackson left a 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.
Copy link
Contributor

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(
Copy link
Contributor

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);
Copy link
Contributor

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.

@collinjackson
Copy link
Contributor

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

kroikie pushed a commit to collinjackson/flutterfire that referenced this pull request Sep 4, 2019
kroikie added a commit to firebase/flutterfire that referenced this pull request Sep 5, 2019
[firebase_messaging] Add Android support for handling messages in background
ryohorie added a commit to ryohorie/flutterfire that referenced this pull request Sep 9, 2019
* 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

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?

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.

@brunoripa
Copy link

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 ?

kroikie pushed a commit to collinjackson/flutterfire that referenced this pull request Nov 15, 2019
nksteven pushed a commit to nksteven/ntf-firebase_messaging that referenced this pull request Apr 8, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.