From ddeb94eb809aeddda170fd97fa992abaf9ea0121 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Wed, 18 Oct 2023 17:38:16 -0700 Subject: [PATCH] wipref notif: Handle when app in background, too Sadly this does not work very well if the app isn't running at all: e.g., if you terminate the app by swiping it away in the app switcher. In that case the notification can be quite a bit delayed. But fixing that seems likely to require some deeper debugging, and getting our hands into Java or Kotlin code for I think the first time in zulip-flutter. So we'll deal with that as a followup issue, 342. --- lib/notifications.dart | 51 ++++++++++++++++++++++++++++++++++-- test/notifications_test.dart | 35 ++++++++++++++++++------- 2 files changed, 74 insertions(+), 12 deletions(-) diff --git a/lib/notifications.dart b/lib/notifications.dart index ca63b860235..7e6735d0dd5 100644 --- a/lib/notifications.dart +++ b/lib/notifications.dart @@ -19,8 +19,22 @@ class NotificationService { static void debugReset() { instance.token.dispose(); _instance = null; + assert(debugBackgroundIsolateIsLive = true); } + /// Whether a background isolate should initialize [LiveZulipBinding]. + /// + /// Ordinarily a [ZulipBinding.firebaseMessagingOnBackgroundMessage] callback + /// will be invoked in a background isolate where it must set up its + /// [ZulipBinding], just as the `main` function does for most of the app. + /// Consequently, by default we have that callback initialize + /// [LiveZulipBinding], just like `main` does. + /// + /// In a test that behavior is undesirable. Tests that will cause + /// [ZulipBinding.firebaseMessagingOnBackgroundMessage] callbacks + /// to get invoked should therefore set this to false. + static bool debugBackgroundIsolateIsLive = true; + /// The FCM registration token for this install of the app. /// /// This is unique to the (app, device) pair, but not permanent. @@ -41,7 +55,8 @@ class NotificationService { // (in order to avoid calling for permissions) await NotificationDisplayManager._init(); - ZulipBinding.instance.firebaseMessagingOnMessage.listen(_onRemoteMessage); + ZulipBinding.instance.firebaseMessagingOnMessage.listen(_onForegroundMessage); + ZulipBinding.instance.firebaseMessagingOnBackgroundMessage(_onBackgroundMessage); // Get the FCM registration token, now and upon changes. See FCM API docs: // https://firebase.google.com/docs/cloud-messaging/android/client#sample-register @@ -71,8 +86,40 @@ class NotificationService { token.value = value; } - static void _onRemoteMessage(FirebaseRemoteMessage message) { + static void _onForegroundMessage(FirebaseRemoteMessage message) { assert(debugLog("notif message: ${message.data}")); + _onRemoteMessage(message); + } + + static Future _onBackgroundMessage(FirebaseRemoteMessage message) async { + // This callback will run in a separate isolate from the rest of the app. + // See docs: + // https://firebase.flutter.dev/docs/messaging/usage/#background-messages + _initBackgroundIsolate(); + + assert(debugLog("notif message in background: ${message.data}")); + _onRemoteMessage(message); + } + + static void _initBackgroundIsolate() { + bool isolateIsLive = true; + assert(() { + isolateIsLive = debugBackgroundIsolateIsLive; + return true; + }()); + if (!isolateIsLive) { + return; + } + + assert(() { + debugLogEnabled = true; + return true; + }()); + LiveZulipBinding.ensureInitialized(); + NotificationDisplayManager._init(); // TODO call this just once per isolate + } + + static void _onRemoteMessage(FirebaseRemoteMessage message) { final data = FcmMessage.fromJson(message.data); switch (data) { case MessageFcmMessage(): NotificationDisplayManager._onMessageFcmMessage(data, message.data); diff --git a/test/notifications_test.dart b/test/notifications_test.dart index 40c37cd75e4..0396f4caa81 100644 --- a/test/notifications_test.dart +++ b/test/notifications_test.dart @@ -68,6 +68,7 @@ void main() { addTearDown(testBinding.reset); testBinding.firebaseMessagingInitialToken = '012abc'; addTearDown(NotificationService.debugReset); + NotificationService.debugBackgroundIsolateIsLive = false; await NotificationService.instance.start(); } @@ -93,13 +94,10 @@ void main() { }); group('NotificationDisplayManager', () { - Future checkNotification(MessageFcmMessage data, { + void checkNotification(MessageFcmMessage data, { required String expectedTitle, required String expectedTagComponent, - }) async { - testBinding.firebaseMessaging.onMessage.add( - RemoteMessage(data: data.toJson())); - await null; + }) { check(testBinding.notifications.takeShowCalls()).single ..id.equals(NotificationDisplayManager.kNotificationId) ..title.equals(expectedTitle) @@ -112,11 +110,28 @@ void main() { ); } + Future checkNotifications(MessageFcmMessage data, { + required String expectedTitle, + required String expectedTagComponent, + }) async { + testBinding.firebaseMessaging.onMessage.add( + RemoteMessage(data: data.toJson())); + await null; + checkNotification(data, expectedTitle: expectedTitle, + expectedTagComponent: expectedTagComponent); + + testBinding.firebaseMessaging.onBackgroundMessage.add( + RemoteMessage(data: data.toJson())); + await null; + checkNotification(data, expectedTitle: expectedTitle, + expectedTagComponent: expectedTagComponent); + } + test('stream message', () async { await init(); final stream = eg.stream(); final message = eg.streamMessage(stream: stream); - await checkNotification(messageFcmMessage(message, streamName: stream.name), + await checkNotifications(messageFcmMessage(message, streamName: stream.name), expectedTitle: '${stream.name} > ${message.subject}', expectedTagComponent: 'stream:${message.streamId}:${message.subject}'); }); @@ -125,7 +140,7 @@ void main() { await init(); final stream = eg.stream(); final message = eg.streamMessage(stream: stream); - await checkNotification(messageFcmMessage(message, streamName: null), + await checkNotifications(messageFcmMessage(message, streamName: null), expectedTitle: '(unknown stream) > ${message.subject}', expectedTagComponent: 'stream:${message.streamId}:${message.subject}'); }); @@ -133,7 +148,7 @@ void main() { test('group DM', () async { await init(); final message = eg.dmMessage(from: eg.thirdUser, to: [eg.otherUser, eg.selfUser]); - await checkNotification(messageFcmMessage(message), + await checkNotifications(messageFcmMessage(message), expectedTitle: "${eg.thirdUser.fullName} to you and 1 others", expectedTagComponent: 'dm:${message.allRecipientIds.join(",")}'); }); @@ -141,7 +156,7 @@ void main() { test('1:1 DM', () async { await init(); final message = eg.dmMessage(from: eg.otherUser, to: [eg.selfUser]); - await checkNotification(messageFcmMessage(message), + await checkNotifications(messageFcmMessage(message), expectedTitle: eg.otherUser.fullName, expectedTagComponent: 'dm:${message.allRecipientIds.join(",")}'); }); @@ -149,7 +164,7 @@ void main() { test('self-DM', () async { await init(); final message = eg.dmMessage(from: eg.selfUser, to: []); - await checkNotification(messageFcmMessage(message), + await checkNotifications(messageFcmMessage(message), expectedTitle: eg.selfUser.fullName, expectedTagComponent: 'dm:${message.allRecipientIds.join(",")}'); });