diff --git a/packages/firebase_messaging/CHANGELOG.md b/packages/firebase_messaging/CHANGELOG.md index 0ab9e24a0793..8848dfc5f5d1 100644 --- a/packages/firebase_messaging/CHANGELOG.md +++ b/packages/firebase_messaging/CHANGELOG.md @@ -1,3 +1,9 @@ +## 8.0.0 + +* Refactors the Android portion for background messages. It is now using a + `FlutterEngine` internally, which does removes the requirement to manually + register a plugin registrant or include FCM dependencies in the host App. + ## 7.0.0 * Depend on `firebase_core` and migrate plugin to use `firebase_core` native SDK versioning features; diff --git a/packages/firebase_messaging/README.md b/packages/firebase_messaging/README.md index 14313e6b489e..f7012244b5af 100644 --- a/packages/firebase_messaging/README.md +++ b/packages/firebase_messaging/README.md @@ -61,55 +61,6 @@ for more. By default background messaging is not enabled. To handle messages in the background: -1. Add the `com.google.firebase:firebase-messaging` dependency in your app-level `build.gradle` file that is typically located at `/android/app/build.gradle`. - - ```gradle - dependencies { - // ... - - implementation 'com.google.firebase:firebase-messaging:' - } - ``` - - Note: you can find out what the latest version of the plugin is [here ("Cloud Messaging")](https://firebase.google.com/support/release-notes/android#latest_sdk_versions). - -1. Add an `Application.java` class to your app in the same directory as your `MainActivity.java`. This is typically found in `/android/app/src/main/java//`. - - ```java - 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. In `Application.java`, make sure to change `package io.flutter.plugins.firebasemessagingexample;` to your package's identifier. Your package's identifier should be something like `com.domain.myapplication`. - - ```java - package com.domain.myapplication; - ``` - -1. Set name property of application in `AndroidManifest.xml`. This is typically found in `/android/app/src/main/`. - - ```xml - - ``` - 1. Define a **TOP-LEVEL** or **STATIC** function to handle background messages ```dart diff --git a/packages/firebase_messaging/android/src/main/java/io/flutter/plugins/firebasemessaging/FirebaseMessagingPlugin.java b/packages/firebase_messaging/android/src/main/java/io/flutter/plugins/firebasemessaging/FirebaseMessagingPlugin.java index d475b9e0a3b8..c1bab2ba4ad0 100644 --- a/packages/firebase_messaging/android/src/main/java/io/flutter/plugins/firebasemessaging/FirebaseMessagingPlugin.java +++ b/packages/firebase_messaging/android/src/main/java/io/flutter/plugins/firebasemessaging/FirebaseMessagingPlugin.java @@ -29,6 +29,8 @@ import io.flutter.plugin.common.MethodChannel.Result; import io.flutter.plugin.common.PluginRegistry.NewIntentListener; import io.flutter.plugin.common.PluginRegistry.Registrar; +import io.flutter.plugin.common.PluginRegistry.ViewDestroyListener; +import io.flutter.view.FlutterNativeView; import java.io.IOException; import java.util.HashMap; import java.util.Map; @@ -41,31 +43,33 @@ public class FirebaseMessagingPlugin extends BroadcastReceiver private static final String TAG = "FirebaseMessagingPlugin"; private MethodChannel channel; - private Context applicationContext; private Activity mainActivity; - public static void registerWith(Registrar registrar) { - FirebaseMessagingPlugin instance = new FirebaseMessagingPlugin(); + public static void registerWith(final Registrar registrar) { + final FirebaseMessagingPlugin instance = new FirebaseMessagingPlugin(); instance.setActivity(registrar.activity()); registrar.addNewIntentListener(instance); instance.onAttachedToEngine(registrar.context(), registrar.messenger()); + registrar.addViewDestroyListener( + new ViewDestroyListener() { + @Override + public boolean onViewDestroy(FlutterNativeView view) { + instance.onDetachedFromEngine(registrar.context()); + return false; + } + }); } private void onAttachedToEngine(Context context, BinaryMessenger binaryMessenger) { - this.applicationContext = context; channel = new MethodChannel(binaryMessenger, "plugins.flutter.io/firebase_messaging"); - final MethodChannel backgroundCallbackChannel = - new MethodChannel(binaryMessenger, "plugins.flutter.io/firebase_messaging_background"); channel.setMethodCallHandler(this); - backgroundCallbackChannel.setMethodCallHandler(this); - FlutterFirebaseMessagingService.setBackgroundChannel(backgroundCallbackChannel); // Register broadcast receiver IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(FlutterFirebaseMessagingService.ACTION_TOKEN); intentFilter.addAction(FlutterFirebaseMessagingService.ACTION_REMOTE_MESSAGE); - LocalBroadcastManager manager = LocalBroadcastManager.getInstance(applicationContext); + LocalBroadcastManager manager = LocalBroadcastManager.getInstance(context); manager.registerReceiver(this, intentFilter); } @@ -80,7 +84,11 @@ public void onAttachedToEngine(FlutterPluginBinding binding) { @Override public void onDetachedFromEngine(FlutterPluginBinding binding) { - LocalBroadcastManager.getInstance(binding.getApplicationContext()).unregisterReceiver(this); + onDetachedFromEngine(binding.getApplicationContext()); + } + + private void onDetachedFromEngine(Context applicationContext) { + LocalBroadcastManager.getInstance(applicationContext).unregisterReceiver(this); } @Override @@ -149,14 +157,10 @@ public void onMethodCall(final MethodCall call, final Result result) { /* 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` is setup to handle 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. */ if ("FcmDartService#start".equals(call.method)) { long setupCallbackHandle = 0; @@ -171,12 +175,10 @@ public void onMethodCall(final MethodCall call, final Result result) { e.printStackTrace(); } FlutterFirebaseMessagingService.setBackgroundSetupHandle(mainActivity, setupCallbackHandle); - FlutterFirebaseMessagingService.startBackgroundIsolate(mainActivity, setupCallbackHandle); FlutterFirebaseMessagingService.setBackgroundMessageHandle( mainActivity, backgroundMessageHandle); - result.success(true); - } else if ("FcmDartService#initialized".equals(call.method)) { - FlutterFirebaseMessagingService.onInitialized(); + mainActivity.startService(new Intent(mainActivity, FlutterFirebaseMessagingService.class)); + result.success(true); } else if ("configure".equals(call.method)) { FirebaseInstanceId.getInstance() diff --git a/packages/firebase_messaging/android/src/main/java/io/flutter/plugins/firebasemessaging/FlutterFirebaseMessagingService.java b/packages/firebase_messaging/android/src/main/java/io/flutter/plugins/firebasemessaging/FlutterFirebaseMessagingService.java index bf9207649bbc..06715786039c 100644 --- a/packages/firebase_messaging/android/src/main/java/io/flutter/plugins/firebasemessaging/FlutterFirebaseMessagingService.java +++ b/packages/firebase_messaging/android/src/main/java/io/flutter/plugins/firebasemessaging/FlutterFirebaseMessagingService.java @@ -12,23 +12,26 @@ import android.os.Handler; import android.os.Process; import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import com.google.firebase.messaging.FirebaseMessagingService; import com.google.firebase.messaging.RemoteMessage; +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.embedding.engine.dart.DartExecutor; +import io.flutter.embedding.engine.dart.DartExecutor.DartCallback; +import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.PluginRegistry; +import io.flutter.plugin.common.MethodChannel.MethodCallHandler; +import io.flutter.plugin.common.MethodChannel.Result; import io.flutter.view.FlutterCallbackInformation; import io.flutter.view.FlutterMain; -import io.flutter.view.FlutterNativeView; -import io.flutter.view.FlutterRunArguments; import java.util.Collections; import java.util.HashMap; -import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.atomic.AtomicBoolean; public class FlutterFirebaseMessagingService extends FirebaseMessagingService { @@ -44,37 +47,41 @@ public class FlutterFirebaseMessagingService extends FirebaseMessagingService { private static final String BACKGROUND_MESSAGE_CALLBACK_HANDLE_KEY = "background_message_callback"; - // TODO(kroikie): make isIsolateRunning per-instance, not static. - private static AtomicBoolean isIsolateRunning = new AtomicBoolean(false); - /** Background Dart execution context. */ - private static FlutterNativeView backgroundFlutterView; + @Nullable private FlutterEngine backgroundEngine; - private static MethodChannel backgroundChannel; + @Nullable private MethodChannel backgroundChannel; private static Long backgroundMessageHandle; - private static List backgroundMessageQueue = + private final List backgroundMessageQueue = Collections.synchronizedList(new LinkedList()); - private static PluginRegistry.PluginRegistrantCallback pluginRegistrantCallback; - private static final String TAG = "FlutterFcmService"; - private static Context backgroundContext; - @Override public void onCreate() { super.onCreate(); - backgroundContext = getApplicationContext(); - FlutterMain.ensureInitializationComplete(backgroundContext, null); + FlutterMain.ensureInitializationComplete(this, null); - // If background isolate is not running start it. - if (!isIsolateRunning.get()) { - SharedPreferences p = backgroundContext.getSharedPreferences(SHARED_PREFERENCES_KEY, 0); - long callbackHandle = p.getLong(BACKGROUND_SETUP_CALLBACK_HANDLE_KEY, 0); - startBackgroundIsolate(backgroundContext, callbackHandle); + if (maybeStartIsolate()) { + Log.d(TAG, "Background isolate has been started."); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + + if (backgroundChannel != null) { + backgroundChannel.setMethodCallHandler(null); + backgroundChannel = null; + } + + if (backgroundEngine != null) { + backgroundEngine.destroy(); + backgroundEngine = null; } } @@ -84,7 +91,7 @@ public void onCreate() { * @param remoteMessage Object representing the message received from Firebase Cloud Messaging. */ @Override - public void onMessageReceived(final RemoteMessage remoteMessage) { + public void onMessageReceived(@NonNull final RemoteMessage remoteMessage) { // If application is running in the foreground use local broadcast to handle message. // Otherwise use the background isolate to handle message. if (isApplicationForeground(this)) { @@ -94,8 +101,10 @@ public void onMessageReceived(final RemoteMessage remoteMessage) { } else { // If background isolate is not running yet, put message in queue and it will be handled // when the isolate starts. - if (!isIsolateRunning.get()) { + if (backgroundEngine == null) { backgroundMessageQueue.add(remoteMessage); + + maybeStartIsolate(); } else { final CountDownLatch latch = new CountDownLatch(1); new Handler(getMainLooper()) @@ -123,12 +132,33 @@ public void run() { * the same as the one retrieved by getInstanceId(). */ @Override - public void onNewToken(String token) { + public void onNewToken(@NonNull String token) { Intent intent = new Intent(ACTION_TOKEN); intent.putExtra(EXTRA_TOKEN, token); LocalBroadcastManager.getInstance(this).sendBroadcast(intent); } + /** + * Only start the isolate if we have background details. + * + * @return Whether the isolate has been start or is running already. + */ + private boolean maybeStartIsolate() { + // If background isolate is not running start it. + SharedPreferences p = getSharedPreferences(SHARED_PREFERENCES_KEY, 0); + long callbackHandle = p.getLong(BACKGROUND_SETUP_CALLBACK_HANDLE_KEY, 0); + + if (backgroundEngine != null) { + return true; + } + if (callbackHandle != 0) { + startBackgroundIsolate(this, callbackHandle); + return true; + } + + return false; + } + /** * Setup the background isolate that would allow background messages to be handled on the Dart * side. Called either by the plugin when the app is starting up or when the app receives a @@ -138,60 +168,52 @@ public void onNewToken(String token) { * @param callbackHandle Handle used to retrieve the Dart function that sets up background * handling on the dart side. */ - public static void startBackgroundIsolate(Context context, long callbackHandle) { + private void startBackgroundIsolate(Context context, long callbackHandle) { FlutterMain.ensureInitializationComplete(context, null); String appBundlePath = FlutterMain.findAppBundlePath(); FlutterCallbackInformation flutterCallback = FlutterCallbackInformation.lookupCallbackInformation(callbackHandle); - if (flutterCallback == null) { - Log.e(TAG, "Fatal: failed to find callback"); - return; - } - // Note that we're passing `true` as the second argument to our - // FlutterNativeView constructor. This specifies the FlutterNativeView - // as a background view and does not create a drawing surface. - backgroundFlutterView = new FlutterNativeView(context, true); - if (appBundlePath != null) { - if (pluginRegistrantCallback == null) { - throw new RuntimeException("PluginRegistrantCallback is not set."); - } - FlutterRunArguments args = new FlutterRunArguments(); - args.bundlePath = appBundlePath; - args.entrypoint = flutterCallback.callbackName; - args.libraryPath = flutterCallback.callbackLibraryPath; - backgroundFlutterView.runFromBundle(args); - pluginRegistrantCallback.registerWith(backgroundFlutterView.getPluginRegistry()); - } + backgroundEngine = new FlutterEngine(context); + + final DartExecutor executor = backgroundEngine.getDartExecutor(); + + backgroundChannel = + new MethodChannel(executor, "plugins.flutter.io/firebase_messaging_background"); + backgroundChannel.setMethodCallHandler( + new MethodCallHandler() { + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { + if ("FcmDartService#initialized".equals(call.method)) { + onInitialized(); + result.success(true); + } else { + result.notImplemented(); + } + } + }); + + final DartCallback dartCallback = + new DartExecutor.DartCallback(getAssets(), appBundlePath, flutterCallback); + + executor.executeDartCallback(dartCallback); } /** * 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`. */ - public static void onInitialized() { - isIsolateRunning.set(true); + public void onInitialized() { synchronized (backgroundMessageQueue) { // Handle all the messages received before the Dart isolate was // initialized, then clear the queue. - Iterator i = backgroundMessageQueue.iterator(); - while (i.hasNext()) { - executeDartCallbackInBackgroundIsolate(backgroundContext, i.next(), null); + for (RemoteMessage remoteMessage : backgroundMessageQueue) { + executeDartCallbackInBackgroundIsolate(this, remoteMessage, null); } backgroundMessageQueue.clear(); } } - /** - * Set the method channel that is used for handling background messages. This method is only - * called when the plugin registers. - * - * @param channel Background method channel. - */ - public static void setBackgroundChannel(MethodChannel channel) { - backgroundChannel = channel; - } - /** * Set the background message handle for future use. When background messages need to be handled * on the Dart side the handler must be retrieved in the background isolate to allow processing of @@ -253,7 +275,7 @@ public static Long getBackgroundMessageHandle(Context context) { * @param latch If set will count down when the Dart side message processing is complete. Allowing * any waiting threads to continue. */ - private static void executeDartCallbackInBackgroundIsolate( + private void executeDartCallbackInBackgroundIsolate( Context context, RemoteMessage remoteMessage, final CountDownLatch latch) { if (backgroundChannel == null) { throw new RuntimeException( @@ -273,9 +295,8 @@ private static void executeDartCallbackInBackgroundIsolate( } args.put("handle", backgroundMessageHandle); - if (remoteMessage.getData() != null) { - messageData.put("data", remoteMessage.getData()); - } + messageData.put("data", remoteMessage.getData()); + if (remoteMessage.getNotification() != null) { messageData.put("notification", remoteMessage.getNotification()); } @@ -285,16 +306,6 @@ private static void executeDartCallbackInBackgroundIsolate( backgroundChannel.invokeMethod("handleBackgroundMessage", args, result); } - /** - * Set the registrant callback. This is called by the app's Application class if background - * message handling is enabled. - * - * @param callback Application class which implements PluginRegistrantCallback. - */ - public static void setPluginRegistrant(PluginRegistry.PluginRegistrantCallback callback) { - pluginRegistrantCallback = callback; - } - /** * Identify if the application is currently in a state where user interaction is possible. This * method is only called by FlutterFirebaseMessagingService when a message is received to diff --git a/packages/firebase_messaging/example/android/app/src/main/AndroidManifest.xml b/packages/firebase_messaging/example/android/app/src/main/AndroidManifest.xml index b1f013664cbe..8eb270a897fc 100644 --- a/packages/firebase_messaging/example/android/app/src/main/AndroidManifest.xml +++ b/packages/firebase_messaging/example/android/app/src/main/AndroidManifest.xml @@ -4,12 +4,12 @@ - + @@ -24,11 +24,8 @@ android:theme="@android:style/Theme.Black.NoTitleBar" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection" android:hardwareAccelerated="true" - android:windowSoftInputMode="adjustResize"> - - - - - + android:windowSoftInputMode="adjustResize"/> + + diff --git a/packages/firebase_messaging/example/android/app/src/main/java/io/flutter/plugins/firebasemessagingexample/EmbeddingV1Activity.java b/packages/firebase_messaging/example/android/app/src/main/java/io/flutter/plugins/firebasemessagingexample/EmbeddingV1Activity.java index cf57491a87f2..61bc7fa41a06 100644 --- a/packages/firebase_messaging/example/android/app/src/main/java/io/flutter/plugins/firebasemessagingexample/EmbeddingV1Activity.java +++ b/packages/firebase_messaging/example/android/app/src/main/java/io/flutter/plugins/firebasemessagingexample/EmbeddingV1Activity.java @@ -1,13 +1,17 @@ package io.flutter.plugins.firebasemessagingexample; import android.os.Bundle; +import dev.flutter.plugins.e2e.E2EPlugin; import io.flutter.app.FlutterActivity; -import io.flutter.plugins.GeneratedPluginRegistrant; +import io.flutter.plugins.firebasemessaging.FirebaseMessagingPlugin; public class EmbeddingV1Activity extends FlutterActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - GeneratedPluginRegistrant.registerWith(this); + + FirebaseMessagingPlugin.registerWith( + registrarFor("io.flutter.plugins.firebasemessaging.FirebaseMessagingPlugin")); + E2EPlugin.registerWith(registrarFor("dev.flutter.plugins.e2e.E2EPlugin")); } } diff --git a/packages/firebase_messaging/example/android/app/src/main/java/io/flutter/plugins/firebasemessagingexample/MainActivity.java b/packages/firebase_messaging/example/android/app/src/main/java/io/flutter/plugins/firebasemessagingexample/MainActivity.java deleted file mode 100644 index 733f2218974e..000000000000 --- a/packages/firebase_messaging/example/android/app/src/main/java/io/flutter/plugins/firebasemessagingexample/MainActivity.java +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.firebasemessagingexample; - -import dev.flutter.plugins.e2e.E2EPlugin; -import io.flutter.embedding.android.FlutterActivity; -import io.flutter.embedding.engine.FlutterEngine; -import io.flutter.plugins.firebase.core.FlutterFirebaseCorePlugin; -import io.flutter.plugins.firebasemessaging.FirebaseMessagingPlugin; - -public class MainActivity extends FlutterActivity { - - // TODO(kroikie): Remove this once v2 of GeneratedPluginRegistrant rolls to stable. https://github.com/flutter/flutter/issues/42694 - @Override - public void configureFlutterEngine(FlutterEngine flutterEngine) { - flutterEngine.getPlugins().add(new FirebaseMessagingPlugin()); - flutterEngine.getPlugins().add(new FlutterFirebaseCorePlugin()); - flutterEngine.getPlugins().add(new E2EPlugin()); - } -} diff --git a/packages/firebase_messaging/example/lib/main.dart b/packages/firebase_messaging/example/lib/main.dart index d248b9c5cbc6..9f798f427cc8 100644 --- a/packages/firebase_messaging/example/lib/main.dart +++ b/packages/firebase_messaging/example/lib/main.dart @@ -7,6 +7,12 @@ import 'dart:async'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; +Future _backgroundMessageHandler(Map message) async { + print('Firebase Messaging in Background: $message'); + + return null; +} + final Map _items = {}; Item _itemForMessage(Map message) { final dynamic data = message['data'] ?? message; @@ -139,6 +145,7 @@ class _PushMessagingExampleState extends State { void initState() { super.initState(); _firebaseMessaging.configure( + onBackgroundMessage: _backgroundMessageHandler, onMessage: (Map message) async { print("onMessage: $message"); _showItemDialog(message); diff --git a/packages/firebase_messaging/pubspec.yaml b/packages/firebase_messaging/pubspec.yaml index 1cb92e81b624..84e63a500d64 100644 --- a/packages/firebase_messaging/pubspec.yaml +++ b/packages/firebase_messaging/pubspec.yaml @@ -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.0 +version: 8.0.0 flutter: plugin: