diff --git a/packages/firebase_messaging/README.md b/packages/firebase_messaging/README.md
index fced940d7b7f..47676faac17d 100644
--- a/packages/firebase_messaging/README.md
+++ b/packages/firebase_messaging/README.md
@@ -54,7 +54,77 @@ Note: When you are debugging on Android, use a device or AVD with Google Play se
```
-
+#### Optionally handle background messages
+
+By default background messaging is not enabled. To handle messages in the background:
+
+1. Add an Application.java class to your app
+
+ ```
+ 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. Set name property of application in `AndroidManifest.xml`
+ ```
+
+ ```
+1. Define a top level Dart method to handle background messages
+ ```
+ Future myBackgroundMessageHandler(Map message) {
+ if (message.containsKey('data')) {
+ // Handle data message
+ dynamic data = message['data'];
+ }
+
+ if (message.containsKey('notification')) {
+ // Handle notification message
+ dynamic notification = message['notification'];
+ }
+
+ // Or do other work.
+ }
+ ```
+ Note: the protocol of `data` and `notification` are in line with the
+ fields defined by a [RemoteMessage](https://firebase.google.com/docs/reference/android/com/google/firebase/messaging/RemoteMessage).
+1. Set `onBackgroundMessage` handler when calling `configure`
+ ```
+ _firebaseMessaging.configure(
+ onMessage: (Map message) async {
+ print("onMessage: $message");
+ _showItemDialog(message);
+ },
+ onBackgroundMessage: myBackgroundMessageHandler,
+ onLaunch: (Map message) async {
+ print("onLaunch: $message");
+ _navigateToItemDetail(message);
+ },
+ onResume: (Map message) async {
+ print("onResume: $message");
+ _navigateToItemDetail(message);
+ },
+ );
+ ```
+ Note: `configure` should be called early in the lifecycle of your application
+ so that it can be ready to receive messages as early as possible. See the
+ example app for a demonstration.
### iOS Integration
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 4ab56e01d299..28e7eb2f1152 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
@@ -41,9 +41,15 @@ public class FirebaseMessagingPlugin extends BroadcastReceiver
public static void registerWith(Registrar registrar) {
final MethodChannel channel =
new MethodChannel(registrar.messenger(), "plugins.flutter.io/firebase_messaging");
+ final MethodChannel backgroundCallbackChannel =
+ new MethodChannel(
+ registrar.messenger(), "plugins.flutter.io/firebase_messaging_background");
final FirebaseMessagingPlugin plugin = new FirebaseMessagingPlugin(registrar, channel);
registrar.addNewIntentListener(plugin);
channel.setMethodCallHandler(plugin);
+ backgroundCallbackChannel.setMethodCallHandler(plugin);
+
+ FlutterFirebaseMessagingService.setBackgroundChannel(backgroundCallbackChannel);
}
private FirebaseMessagingPlugin(Registrar registrar, MethodChannel channel) {
@@ -99,7 +105,40 @@ private Map parseRemoteMessage(RemoteMessage message) {
@Override
public void onMethodCall(final MethodCall call, final Result result) {
- if ("configure".equals(call.method)) {
+ /* 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.
+ */
+ if ("FcmDartService#start".equals(call.method)) {
+ long setupCallbackHandle = 0;
+ long backgroundMessageHandle = 0;
+ try {
+ Map callbacks = ((Map) call.arguments);
+ setupCallbackHandle = callbacks.get("setupHandle");
+ backgroundMessageHandle = callbacks.get("backgroundHandle");
+ } catch (Exception e) {
+ Log.e(TAG, "There was an exception when getting callback handle from Dart side");
+ e.printStackTrace();
+ }
+ FlutterFirebaseMessagingService.setBackgroundSetupHandle(
+ this.registrar.context(), setupCallbackHandle);
+ FlutterFirebaseMessagingService.startBackgroundIsolate(
+ this.registrar.context(), setupCallbackHandle);
+ FlutterFirebaseMessagingService.setBackgroundMessageHandle(
+ this.registrar.context(), backgroundMessageHandle);
+ result.success(true);
+ } else if ("FcmDartService#initialized".equals(call.method)) {
+ FlutterFirebaseMessagingService.onInitialized();
+ result.success(true);
+ } else if ("configure".equals(call.method)) {
FirebaseInstanceId.getInstance()
.getInstanceId()
.addOnCompleteListener(
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 345c0161e192..97d898dcc8f2 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
@@ -4,10 +4,31 @@
package io.flutter.plugins.firebasemessaging;
+import android.app.ActivityManager;
+import android.app.KeyguardManager;
+import android.content.Context;
import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.Handler;
+import android.os.Process;
+import android.util.Log;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;
+import io.flutter.plugin.common.MethodChannel;
+import io.flutter.plugin.common.PluginRegistry;
+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 {
@@ -18,16 +39,81 @@ public class FlutterFirebaseMessagingService extends FirebaseMessagingService {
public static final String ACTION_TOKEN = "io.flutter.plugins.firebasemessaging.TOKEN";
public static final String EXTRA_TOKEN = "token";
+ private static final String SHARED_PREFERENCES_KEY = "io.flutter.android_fcm_plugin";
+ private static final String BACKGROUND_SETUP_CALLBACK_HANDLE_KEY = "background_setup_callback";
+ private static final String BACKGROUND_MESSAGE_CALLBACK_HANDLE_KEY =
+ "background_message_callback";
+
+ // TODO(kroikie): make sIsIsolateRunning per-instance, not static.
+ private static AtomicBoolean sIsIsolateRunning = new AtomicBoolean(false);
+
+ /** Background Dart execution context. */
+ private static FlutterNativeView sBackgroundFlutterView;
+
+ private static MethodChannel sBackgroundChannel;
+
+ private static Long sBackgroundMessageHandle;
+
+ private static List sBackgroundMessageQueue =
+ Collections.synchronizedList(new LinkedList());
+
+ private static PluginRegistry.PluginRegistrantCallback sPluginRegistrantCallback;
+
+ private static final String TAG = "FlutterFcmService";
+
+ private static Context sBackgroundContext;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ sBackgroundContext = getApplicationContext();
+ FlutterMain.ensureInitializationComplete(sBackgroundContext, null);
+
+ // If background isolate is not running start it.
+ if (!sIsIsolateRunning.get()) {
+ SharedPreferences p = sBackgroundContext.getSharedPreferences(SHARED_PREFERENCES_KEY, 0);
+ long callbackHandle = p.getLong(BACKGROUND_SETUP_CALLBACK_HANDLE_KEY, 0);
+ startBackgroundIsolate(sBackgroundContext, callbackHandle);
+ }
+ }
+
/**
* Called when message is received.
*
* @param remoteMessage Object representing the message received from Firebase Cloud Messaging.
*/
@Override
- public void onMessageReceived(RemoteMessage remoteMessage) {
- Intent intent = new Intent(ACTION_REMOTE_MESSAGE);
- intent.putExtra(EXTRA_REMOTE_MESSAGE, remoteMessage);
- LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
+ public void onMessageReceived(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)) {
+ Intent intent = new Intent(ACTION_REMOTE_MESSAGE);
+ intent.putExtra(EXTRA_REMOTE_MESSAGE, remoteMessage);
+ LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
+ } else {
+ // If background isolate is not running yet, put message in queue and it will be handled
+ // when the isolate starts.
+ if (!sIsIsolateRunning.get()) {
+ sBackgroundMessageQueue.add(remoteMessage);
+ } else {
+ final CountDownLatch latch = new CountDownLatch(1);
+ new Handler(getMainLooper())
+ .post(
+ new Runnable() {
+ @Override
+ public void run() {
+ executeDartCallbackInBackgroundIsolate(
+ FlutterFirebaseMessagingService.this, remoteMessage, latch);
+ }
+ });
+ try {
+ latch.await();
+ } catch (InterruptedException ex) {
+ Log.i(TAG, "Exception waiting to execute Dart callback", ex);
+ }
+ }
+ }
}
/**
@@ -42,4 +128,221 @@ public void onNewToken(String token) {
intent.putExtra(EXTRA_TOKEN, token);
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
}
+
+ /**
+ * 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
+ * message while it is inactive.
+ *
+ * @param context Registrar or FirebaseMessagingService context.
+ * @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) {
+ FlutterMain.ensureInitializationComplete(context, null);
+ String appBundlePath = FlutterMain.findAppBundlePath(context);
+ 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.
+ sBackgroundFlutterView = new FlutterNativeView(context, true);
+ if (appBundlePath != null && !sIsIsolateRunning.get()) {
+ if (sPluginRegistrantCallback == null) {
+ throw new RuntimeException("PluginRegistrantCallback is not set.");
+ }
+ FlutterRunArguments args = new FlutterRunArguments();
+ args.bundlePath = appBundlePath;
+ args.entrypoint = flutterCallback.callbackName;
+ args.libraryPath = flutterCallback.callbackLibraryPath;
+ sBackgroundFlutterView.runFromBundle(args);
+ sPluginRegistrantCallback.registerWith(sBackgroundFlutterView.getPluginRegistry());
+ }
+ }
+
+ /**
+ * 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() {
+ sIsIsolateRunning.set(true);
+ synchronized (sBackgroundMessageQueue) {
+ // Handle all the messages received before the Dart isolate was
+ // initialized, then clear the queue.
+ Iterator i = sBackgroundMessageQueue.iterator();
+ while (i.hasNext()) {
+ executeDartCallbackInBackgroundIsolate(sBackgroundContext, i.next(), null);
+ }
+ sBackgroundMessageQueue.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) {
+ sBackgroundChannel = 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
+ * the incoming message. This method is called by the Dart side via `FcmDartService#start`.
+ *
+ * @param context Registrar context.
+ * @param handle Handle representing the Dart side method that will handle background messages.
+ */
+ public static void setBackgroundMessageHandle(Context context, Long handle) {
+ sBackgroundMessageHandle = handle;
+
+ // Store background message handle in shared preferences so it can be retrieved
+ // by other application instances.
+ SharedPreferences prefs = context.getSharedPreferences(SHARED_PREFERENCES_KEY, 0);
+ prefs.edit().putLong(BACKGROUND_MESSAGE_CALLBACK_HANDLE_KEY, handle).apply();
+ }
+
+ /**
+ * Set the background message setup handle for future use. The Dart side of this plugin has a
+ * method that sets up the background method channel. When ready to setup the background channel
+ * the Dart side needs to be able to retrieve the setup method. This method is called by the Dart
+ * side via `FcmDartService#start`.
+ *
+ * @param context Registrar context.
+ * @param setupBackgroundHandle Handle representing the dart side method that will setup the
+ * background method channel.
+ */
+ public static void setBackgroundSetupHandle(Context context, long setupBackgroundHandle) {
+ // Store background setup handle in shared preferences so it can be retrieved
+ // by other application instances.
+ SharedPreferences prefs = context.getSharedPreferences(SHARED_PREFERENCES_KEY, 0);
+ prefs.edit().putLong(BACKGROUND_SETUP_CALLBACK_HANDLE_KEY, setupBackgroundHandle).apply();
+ }
+
+ /**
+ * Retrieve the background message handle. When a background message is received and must be
+ * processed on the dart side the handle representing the Dart side handle is retrieved so the
+ * appropriate method can be called to process the message on the Dart side. This method is called
+ * by FlutterFirebaseMessagingServcie either when a new background message is received or if
+ * background messages were queued up while background message handling was being setup.
+ *
+ * @param context Application context.
+ * @return Dart side background message handle.
+ */
+ public static Long getBackgroundMessageHandle(Context context) {
+ return context
+ .getSharedPreferences(SHARED_PREFERENCES_KEY, 0)
+ .getLong(BACKGROUND_MESSAGE_CALLBACK_HANDLE_KEY, 0);
+ }
+
+ /**
+ * Process the incoming message in the background isolate. This method is called only after
+ * background method channel is setup, it is called by FlutterFirebaseMessagingServcie either when
+ * a new background message is received or after background method channel setup for queued
+ * messages received during setup.
+ *
+ * @param context Application or FirebaseMessagingService context.
+ * @param remoteMessage Message received from Firebase Cloud Messaging.
+ * @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(
+ Context context, RemoteMessage remoteMessage, final CountDownLatch latch) {
+ if (sBackgroundChannel == null) {
+ throw new RuntimeException(
+ "setBackgroundChannel was not called before messages came in, exiting.");
+ }
+
+ // If another thread is waiting, then wake that thread when the callback returns a result.
+ MethodChannel.Result result = null;
+ if (latch != null) {
+ result =
+ new MethodChannel.Result() {
+ @Override
+ public void success(Object result) {
+ latch.countDown();
+ }
+
+ @Override
+ public void error(String errorCode, String errorMessage, Object errorDetails) {
+ latch.countDown();
+ }
+
+ @Override
+ public void notImplemented() {
+ latch.countDown();
+ }
+ };
+ }
+
+ Map args = new HashMap<>();
+ Map messageData = new HashMap<>();
+ if (sBackgroundMessageHandle == null) {
+ sBackgroundMessageHandle = getBackgroundMessageHandle(context);
+ }
+ args.put("handle", sBackgroundMessageHandle);
+
+ if (remoteMessage.getData() != null) {
+ messageData.put("data", remoteMessage.getData());
+ }
+ if (remoteMessage.getNotification() != null) {
+ messageData.put("notification", remoteMessage.getNotification());
+ }
+
+ args.put("message", messageData);
+
+ sBackgroundChannel.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) {
+ sPluginRegistrantCallback = 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
+ * determine how the incoming message should be handled.
+ *
+ * @param context FlutterFirebaseMessagingService context.
+ * @return True if the application is currently in a state where user interaction is possible,
+ * false otherwise.
+ */
+ // TODO(kroikie): Find a better way to determine application state.
+ private static boolean isApplicationForeground(Context context) {
+ KeyguardManager keyguardManager =
+ (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE);
+
+ if (keyguardManager.inKeyguardRestrictedInputMode()) {
+ return false;
+ }
+ int myPid = Process.myPid();
+
+ ActivityManager activityManager =
+ (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+
+ List list;
+
+ if ((list = activityManager.getRunningAppProcesses()) != null) {
+ for (ActivityManager.RunningAppProcessInfo aList : list) {
+ ActivityManager.RunningAppProcessInfo info;
+ if ((info = aList).pid == myPid) {
+ return info.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
+ }
+ }
+ }
+ return false;
+ }
}
diff --git a/packages/firebase_messaging/lib/firebase_messaging.dart b/packages/firebase_messaging/lib/firebase_messaging.dart
index f40420c6dbe5..794aa9980a70 100644
--- a/packages/firebase_messaging/lib/firebase_messaging.dart
+++ b/packages/firebase_messaging/lib/firebase_messaging.dart
@@ -3,13 +3,54 @@
// found in the LICENSE file.
import 'dart:async';
+import 'dart:io';
+import 'dart:ui';
import 'package:flutter/services.dart';
+import 'package:flutter/widgets.dart';
import 'package:meta/meta.dart';
import 'package:platform/platform.dart';
typedef Future MessageHandler(Map message);
+/// 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
+Future 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.from(call.arguments['message']));
+ } catch (e) {
+ print('Unable to handle incoming background message.');
+ print(e);
+ }
+ return Future.value();
+ }
+ });
+
+// Once we've finished initializing, let the native portion of the plugin
+// know that it can start scheduling handling messages.
+ await backgroundChannel.invokeMethod('FcmDartService#initialized');
+}
+
/// Implementation of the Firebase Cloud Messaging API for Flutter.
///
/// Your app should call [requestNotificationPermissions] first and then
@@ -30,6 +71,7 @@ class FirebaseMessaging {
final Platform _platform;
MessageHandler _onMessage;
+ MessageHandler _onBackgroundMessage;
MessageHandler _onLaunch;
MessageHandler _onResume;
@@ -59,6 +101,7 @@ class FirebaseMessaging {
/// Sets up [MessageHandler] for incoming messages.
void configure({
MessageHandler onMessage,
+ MessageHandler onBackgroundMessage,
MessageHandler onLaunch,
MessageHandler onResume,
}) {
@@ -67,6 +110,20 @@ class FirebaseMessaging {
_onResume = onResume;
_channel.setMethodCallHandler(_handleMethod);
_channel.invokeMethod('configure');
+ if (onBackgroundMessage != null) {
+ _onBackgroundMessage = onBackgroundMessage;
+ final CallbackHandle backgroundSetupHandle =
+ PluginUtilities.getCallbackHandle(fcmSetupBackgroundChannel);
+ final CallbackHandle backgroundMessageHandle =
+ PluginUtilities.getCallbackHandle(_onBackgroundMessage);
+ _channel.invokeMethod(
+ 'FcmDartService#start',
+ {
+ 'setupHandle': backgroundSetupHandle.toRawHandle(),
+ 'backgroundHandle': backgroundMessageHandle.toRawHandle()
+ },
+ );
+ }
}
final StreamController _tokenStreamController =
diff --git a/packages/firebase_messaging/test/firebase_messaging_test.dart b/packages/firebase_messaging/test/firebase_messaging_test.dart
index a4e6d7c04756..057d178e4237 100644
--- a/packages/firebase_messaging/test/firebase_messaging_test.dart
+++ b/packages/firebase_messaging/test/firebase_messaging_test.dart
@@ -12,10 +12,12 @@ import 'package:test/test.dart';
void main() {
MockMethodChannel mockChannel;
+ MockMethodChannel mockBackgroundChannel;
FirebaseMessaging firebaseMessaging;
setUp(() {
mockChannel = MockMethodChannel();
+ mockBackgroundChannel = MockMethodChannel();
firebaseMessaging = FirebaseMessaging.private(
mockChannel, FakePlatform(operatingSystem: 'ios'));
});
@@ -163,6 +165,12 @@ void main() {
verify(mockChannel.invokeMethod('setAutoInitEnabled', false));
});
+
+ test('setupBackgroundCallback', () {
+ fcmSetupBackgroundChannel(backgroundChannel: mockBackgroundChannel);
+ verify(
+ mockBackgroundChannel.invokeMethod('FcmDartService#initialized'));
+ });
}
class MockMethodChannel extends Mock implements MethodChannel {}