diff --git a/packages/firebase_messaging/CHANGELOG.md b/packages/firebase_messaging/CHANGELOG.md index 834f14e6c54d..ec983ece19e0 100644 --- a/packages/firebase_messaging/CHANGELOG.md +++ b/packages/firebase_messaging/CHANGELOG.md @@ -1,3 +1,5 @@ +- Support for multiple senderId + ## 7.0.3 - Update a dependency to the latest release. 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..a07cada92af5 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 @@ -13,8 +13,11 @@ import android.util.Log; import androidx.annotation.NonNull; import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import com.google.android.gms.tasks.Continuation; import com.google.android.gms.tasks.OnCompleteListener; import com.google.android.gms.tasks.Task; +import com.google.android.gms.tasks.TaskCompletionSource; +import com.google.android.gms.tasks.Tasks; import com.google.firebase.iid.FirebaseInstanceId; import com.google.firebase.iid.InstanceIdResult; import com.google.firebase.messaging.FirebaseMessaging; @@ -37,6 +40,7 @@ public class FirebaseMessagingPlugin extends BroadcastReceiver implements MethodCallHandler, NewIntentListener, FlutterPlugin, ActivityAware { + private static final String FCM_VALUE = "FCM"; private static final String CLICK_ACTION_VALUE = "FLUTTER_NOTIFICATION_CLICK"; private static final String TAG = "FirebaseMessagingPlugin"; @@ -128,6 +132,7 @@ public void onReceive(Context context, Intent intent) { @NonNull private Map parseRemoteMessage(RemoteMessage message) { Map content = new HashMap<>(); + content.put("senderId", message.getFrom()); content.put("data", message.getData()); RemoteMessage.Notification notification = message.getNotification(); @@ -179,17 +184,17 @@ public void onMethodCall(final MethodCall call, final Result result) { FlutterFirebaseMessagingService.onInitialized(); result.success(true); } else if ("configure".equals(call.method)) { - FirebaseInstanceId.getInstance() - .getInstanceId() + String senderId = (String) call.arguments; + getToken(senderId) .addOnCompleteListener( - new OnCompleteListener() { + new OnCompleteListener() { @Override - public void onComplete(@NonNull Task task) { + public void onComplete(@NonNull Task task) { if (!task.isSuccessful()) { - Log.w(TAG, "getToken, error fetching instanceID: ", task.getException()); + Log.w(TAG, "configure, error fetching instanceID: ", task.getException()); return; } - channel.invokeMethod("onToken", task.getResult().getToken()); + channel.invokeMethod("onToken", task.getResult()); } }); if (mainActivity != null) { @@ -231,28 +236,34 @@ public void onComplete(@NonNull Task task) { } }); } else if ("getToken".equals(call.method)) { - FirebaseInstanceId.getInstance() - .getInstanceId() + String senderId = (String) call.arguments; + getToken(senderId) .addOnCompleteListener( - new OnCompleteListener() { + new OnCompleteListener() { @Override - public void onComplete(@NonNull Task task) { + public void onComplete(@NonNull Task task) { if (!task.isSuccessful()) { Log.w(TAG, "getToken, error fetching instanceID: ", task.getException()); result.success(null); return; } - result.success(task.getResult().getToken()); + result.success(task.getResult()); } }); } else if ("deleteInstanceID".equals(call.method)) { + final String senderId = (String) call.arguments; new Thread( new Runnable() { @Override public void run() { try { - FirebaseInstanceId.getInstance().deleteInstanceId(); + if (senderId != null) { + FirebaseInstanceId.getInstance().deleteToken(senderId, FCM_VALUE); + } else { + FirebaseInstanceId.getInstance().deleteInstanceId(); + } + if (mainActivity != null) { mainActivity.runOnUiThread( new Runnable() { @@ -288,6 +299,39 @@ public void run() { } } + private Task getToken(final String senderId) { + if (senderId == null) { + return FirebaseInstanceId.getInstance() + .getInstanceId() + .continueWithTask( + new Continuation>() { + @Override + public Task then(@NonNull Task task) throws Exception { + if (!task.isSuccessful()) { + return Tasks.forException(task.getException()); + } + InstanceIdResult result = task.getResult(); + return Tasks.forResult(task.getResult().getToken()); + } + }); + } else { + final TaskCompletionSource task = new TaskCompletionSource(); + new Thread( + new Runnable() { + @Override + public void run() { + try { + task.setResult(FirebaseInstanceId.getInstance().getToken(senderId, FCM_VALUE)); + } catch (IOException e) { + task.setException(e); + } + } + }) + .start(); + return task.getTask(); + } + } + @Override public boolean onNewIntent(Intent intent) { boolean res = sendMessageFromIntent("onResume", intent); @@ -318,6 +362,9 @@ private boolean sendMessageFromIntent(String method, Intent intent) { } } + if (dataMap.containsKey("from")) { + message.put("senderId", dataMap.get("from")); + } message.put("notification", notificationMap); message.put("data", dataMap); diff --git a/packages/firebase_messaging/lib/firebase_messaging.dart b/packages/firebase_messaging/lib/firebase_messaging.dart index 81f2018edecd..23293d725ffc 100644 --- a/packages/firebase_messaging/lib/firebase_messaging.dart +++ b/packages/firebase_messaging/lib/firebase_messaging.dart @@ -50,6 +50,13 @@ void _fcmSetupBackgroundChannel( backgroundChannel.invokeMethod('FcmDartService#initialized'); } +class FirebaseMessagingHandlers { + MessageHandler _onMessage; + MessageHandler _onBackgroundMessage; + MessageHandler _onLaunch; + MessageHandler _onResume; +} + /// Implementation of the Firebase Cloud Messaging API for Flutter. /// /// Your app should call [requestNotificationPermissions] first and then @@ -64,11 +71,9 @@ class FirebaseMessaging { const MethodChannel('plugins.flutter.io/firebase_messaging')); final MethodChannel _channel; + bool _initialized = false; - MessageHandler _onMessage; - MessageHandler _onBackgroundMessage; - MessageHandler _onLaunch; - MessageHandler _onResume; + Map _handlers = {}; /// On iOS, prompts the user for notification permissions the first time /// it is called. @@ -97,23 +102,33 @@ class FirebaseMessaging { } /// Sets up [MessageHandler] for incoming messages. + /// Use [senderId] to receive messages from another firebase app. + /// See [Firebase official documentation](https://firebase.google.com/docs/cloud-messaging/concept-options#receiving-messages-from-multiple-senders) void configure({ + String senderId, MessageHandler onMessage, MessageHandler onBackgroundMessage, MessageHandler onLaunch, MessageHandler onResume, }) { - _onMessage = onMessage; - _onLaunch = onLaunch; - _onResume = onResume; - _channel.setMethodCallHandler(_handleMethod); - _channel.invokeMethod('configure'); + if (!_initialized) { + _channel.setMethodCallHandler(_handleMethod); + _initialized = true; + } + + FirebaseMessagingHandlers handlers = FirebaseMessagingHandlers(); + _handlers[senderId] = handlers; + handlers._onMessage = onMessage; + handlers._onLaunch = onLaunch; + handlers._onResume = onResume; + + _channel.invokeMethod('configure', senderId); if (onBackgroundMessage != null) { - _onBackgroundMessage = onBackgroundMessage; + handlers._onBackgroundMessage = onBackgroundMessage; final CallbackHandle backgroundSetupHandle = PluginUtilities.getCallbackHandle(_fcmSetupBackgroundChannel); final CallbackHandle backgroundMessageHandle = - PluginUtilities.getCallbackHandle(_onBackgroundMessage); + PluginUtilities.getCallbackHandle(handlers._onBackgroundMessage); if (backgroundMessageHandle == null) { throw ArgumentError( @@ -123,13 +138,15 @@ class FirebaseMessaging { ); } - _channel.invokeMethod( - 'FcmDartService#start', - { - 'setupHandle': backgroundSetupHandle.toRawHandle(), - 'backgroundHandle': backgroundMessageHandle.toRawHandle() - }, - ); + if (!_initialized) { + _channel.invokeMethod( + 'FcmDartService#start', + { + 'setupHandle': backgroundSetupHandle.toRawHandle(), + 'backgroundHandle': backgroundMessageHandle.toRawHandle() + }, + ); + } } } @@ -142,8 +159,10 @@ class FirebaseMessaging { } /// Returns the FCM token. - Future getToken() async { - return await _channel.invokeMethod('getToken'); + /// Use [senderId] to get the token from another sender. + /// See [Firebase official documentation](https://firebase.google.com/docs/cloud-messaging/concept-options#receiving-messages-from-multiple-senders) + Future getToken({String senderId}) async { + return await _channel.invokeMethod('getToken', senderId); } /// Subscribe to topic in background. @@ -163,9 +182,11 @@ class FirebaseMessaging { /// /// A new Instance ID is generated asynchronously if Firebase Cloud Messaging auto-init is enabled. /// - /// returns true if the operations executed successfully and false if an error ocurred - Future deleteInstanceID() async { - return await _channel.invokeMethod('deleteInstanceID'); + /// returns true if the operations executed successfully and false if an error occurred + /// Use [senderId] remove a secondary sender + Future deleteInstanceID({String senderId}) async { + _handlers.remove(senderId); + return await _channel.invokeMethod('deleteInstanceID', senderId); } /// Determine whether FCM auto-initialization is enabled or disabled. @@ -189,11 +210,20 @@ class FirebaseMessaging { call.arguments.cast())); return null; case "onMessage": - return _onMessage(call.arguments.cast()); + final args = call.arguments.cast(); + String senderId = args['senderId']; + final handlers = _handlers[senderId]; + return handlers._onMessage(args); case "onLaunch": - return _onLaunch(call.arguments.cast()); + final args = call.arguments.cast(); + String senderId = args['senderId']; + final handlers = _handlers[senderId]; + return handlers._onLaunch(call.arguments.cast()); case "onResume": - return _onResume(call.arguments.cast()); + final args = call.arguments.cast(); + String senderId = args['senderId']; + _handlers[senderId]._onResume(args); + return; default: throw UnsupportedError("Unrecognized JSON message"); } diff --git a/packages/firebase_messaging/test/firebase_messaging_test.dart b/packages/firebase_messaging/test/firebase_messaging_test.dart old mode 100644 new mode 100755 index 3cc474e676b7..2ce5c8d9b03d --- a/packages/firebase_messaging/test/firebase_messaging_test.dart +++ b/packages/firebase_messaging/test/firebase_messaging_test.dart @@ -46,85 +46,123 @@ void main() { })); }, skip: defaultTargetPlatform != TargetPlatform.iOS); - test('configure', () { - firebaseMessaging.configure(); - verify(mockChannel.setMethodCallHandler(any)); - verify(mockChannel.invokeMethod('configure')); - }); - - test('incoming token', () async { - firebaseMessaging.configure(); - final dynamic handler = - verify(mockChannel.setMethodCallHandler(captureAny)).captured.single; - final String token1 = 'I am a super secret token'; - final String token2 = 'I am the new token in town'; - Future tokenFromStream = firebaseMessaging.onTokenRefresh.first; - await handler(MethodCall('onToken', token1)); - - expect(await tokenFromStream, token1); - - tokenFromStream = firebaseMessaging.onTokenRefresh.first; - await handler(MethodCall('onToken', token2)); - - expect(await tokenFromStream, token2); - }); - - test('incoming iOS settings', () async { - firebaseMessaging.configure(); - final dynamic handler = - verify(mockChannel.setMethodCallHandler(captureAny)).captured.single; - IosNotificationSettings iosSettings = const IosNotificationSettings(); - - Future iosSettingsFromStream = - firebaseMessaging.onIosSettingsRegistered.first; - await handler(MethodCall('onIosSettingsRegistered', iosSettings.toMap())); - expect((await iosSettingsFromStream).toMap(), iosSettings.toMap()); - - iosSettings = const IosNotificationSettings(sound: false); - iosSettingsFromStream = firebaseMessaging.onIosSettingsRegistered.first; - await handler(MethodCall('onIosSettingsRegistered', iosSettings.toMap())); - expect((await iosSettingsFromStream).toMap(), iosSettings.toMap()); - }, skip: defaultTargetPlatform != TargetPlatform.iOS); - - test('incoming messages', () async { - final Completer onMessage = Completer(); - final Completer onLaunch = Completer(); - final Completer onResume = Completer(); - - firebaseMessaging.configure( - onMessage: (dynamic m) async { - onMessage.complete(m); - }, - onLaunch: (dynamic m) async { - onLaunch.complete(m); - }, - onResume: (dynamic m) async { - onResume.complete(m); - }, - onBackgroundMessage: validOnBackgroundMessage, - ); - final dynamic handler = - verify(mockChannel.setMethodCallHandler(captureAny)).captured.single; - - final Map onMessageMessage = {}; - final Map onLaunchMessage = {}; - final Map onResumeMessage = {}; - - await handler(MethodCall('onMessage', onMessageMessage)); - expect(await onMessage.future, onMessageMessage); - expect(onLaunch.isCompleted, isFalse); - expect(onResume.isCompleted, isFalse); - - await handler(MethodCall('onLaunch', onLaunchMessage)); - expect(await onLaunch.future, onLaunchMessage); - expect(onResume.isCompleted, isFalse); - - await handler(MethodCall('onResume', onResumeMessage)); - expect(await onResume.future, onResumeMessage); - }); + void testsForSenderId(String senderId) { + group('senderId: ${senderId ?? 'default'}', () { + test('configure', () { + firebaseMessaging.configure(senderId: senderId); + verify(mockChannel.setMethodCallHandler(any)); + verify(mockChannel.invokeMethod('configure', senderId)); + }); + + test('incoming token', () async { + firebaseMessaging.configure(senderId: senderId); + final dynamic handler = + verify(mockChannel.setMethodCallHandler(captureAny)) + .captured + .single; + final String token1 = 'I am a super secret token'; + final String token2 = 'I am the new token in town'; + Future tokenFromStream = firebaseMessaging.onTokenRefresh.first; + await handler(MethodCall('onToken', token1)); + + expect(await tokenFromStream, token1); + + tokenFromStream = firebaseMessaging.onTokenRefresh.first; + await handler(MethodCall('onToken', token2)); + + expect(await tokenFromStream, token2); + }); + + test('incoming iOS settings', () async { + firebaseMessaging.configure(senderId: senderId); + final dynamic handler = + verify(mockChannel.setMethodCallHandler(captureAny)) + .captured + .single; + IosNotificationSettings iosSettings = const IosNotificationSettings(); + + Future iosSettingsFromStream = + firebaseMessaging.onIosSettingsRegistered.first; + await handler( + MethodCall('onIosSettingsRegistered', iosSettings.toMap())); + expect((await iosSettingsFromStream).toMap(), iosSettings.toMap()); + + iosSettings = const IosNotificationSettings(sound: false); + iosSettingsFromStream = firebaseMessaging.onIosSettingsRegistered.first; + await handler( + MethodCall('onIosSettingsRegistered', iosSettings.toMap())); + expect((await iosSettingsFromStream).toMap(), iosSettings.toMap()); + }, skip: defaultTargetPlatform != TargetPlatform.iOS); + + test('incoming messages', () async { + final Completer onMessage = Completer(); + final Completer onLaunch = Completer(); + final Completer onResume = Completer(); + + firebaseMessaging.configure( + senderId: senderId, + onMessage: (dynamic m) async { + onMessage.complete(m); + }, + onLaunch: (dynamic m) async { + onLaunch.complete(m); + }, + onResume: (dynamic m) async { + onResume.complete(m); + }, + onBackgroundMessage: validOnBackgroundMessage, + ); + final dynamic handler = + verify(mockChannel.setMethodCallHandler(captureAny)) + .captured + .single; + + final Map onMessageMessage = {}; + onMessageMessage['senderId'] = senderId; + final Map onLaunchMessage = {}; + onLaunchMessage['senderId'] = senderId; + final Map onResumeMessage = {}; + onResumeMessage['senderId'] = senderId; + + await handler(MethodCall('onMessage', onMessageMessage)); + expect(await onMessage.future, onMessageMessage); + expect(onLaunch.isCompleted, isFalse); + expect(onResume.isCompleted, isFalse); + + await handler(MethodCall('onLaunch', onLaunchMessage)); + expect(await onLaunch.future, onLaunchMessage); + expect(onResume.isCompleted, isFalse); + + await handler(MethodCall('onResume', onResumeMessage)); + expect(await onResume.future, onResumeMessage); + }); + + test('getToken', () { + firebaseMessaging.getToken(senderId: senderId); + verify(mockChannel.invokeMethod('getToken', senderId)); + }); + + test('deleteInstanceID', () { + firebaseMessaging.deleteInstanceID(senderId: senderId); + verify(mockChannel.invokeMethod('deleteInstanceID', senderId)); + }); + + test('configure bad onBackgroundMessage', () { + expect( + () => firebaseMessaging.configure( + senderId: senderId, + onBackgroundMessage: (dynamic message) => Future.value(), + ), + throwsArgumentError, + ); + }); + }); + } + + testsForSenderId(null); // Default use case + testsForSenderId("123456789012"); // Secondary senderId const String myTopic = 'Flutter'; - test('subscribe to topic', () async { await firebaseMessaging.subscribeToTopic(myTopic); verify(mockChannel.invokeMethod('subscribeToTopic', myTopic)); @@ -135,16 +173,6 @@ void main() { verify(mockChannel.invokeMethod('unsubscribeFromTopic', myTopic)); }); - test('getToken', () { - firebaseMessaging.getToken(); - verify(mockChannel.invokeMethod('getToken')); - }); - - test('deleteInstanceID', () { - firebaseMessaging.deleteInstanceID(); - verify(mockChannel.invokeMethod('deleteInstanceID')); - }); - test('autoInitEnabled', () { firebaseMessaging.autoInitEnabled(); verify(mockChannel.invokeMethod('autoInitEnabled')); @@ -165,15 +193,6 @@ void main() { verify(mockChannel.invokeMethod('setAutoInitEnabled', false)); }); - - test('configure bad onBackgroundMessage', () { - expect( - () => firebaseMessaging.configure( - onBackgroundMessage: (dynamic message) => Future.value(), - ), - throwsArgumentError, - ); - }); } Future validOnBackgroundMessage(Map message) async {}