Skip to content

Commit

Permalink
wipref notif: Handle when app in background, too
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
gnprice committed Nov 1, 2023
1 parent 85c7bc3 commit ddeb94e
Show file tree
Hide file tree
Showing 2 changed files with 74 additions and 12 deletions.
51 changes: 49 additions & 2 deletions lib/notifications.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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<void> _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);
Expand Down
35 changes: 25 additions & 10 deletions test/notifications_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ void main() {
addTearDown(testBinding.reset);
testBinding.firebaseMessagingInitialToken = '012abc';
addTearDown(NotificationService.debugReset);
NotificationService.debugBackgroundIsolateIsLive = false;
await NotificationService.instance.start();
}

Expand All @@ -93,13 +94,10 @@ void main() {
});

group('NotificationDisplayManager', () {
Future<void> 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)
Expand All @@ -112,11 +110,28 @@ void main() {
);
}

Future<void> 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}');
});
Expand All @@ -125,31 +140,31 @@ 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}');
});

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(",")}');
});

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(",")}');
});

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(",")}');
});
Expand Down

0 comments on commit ddeb94e

Please sign in to comment.