Skip to content

Commit

Permalink
notif: Handle when app in background, too (on Android)
Browse files Browse the repository at this point in the history
This completes the core functionality of showing notifications on
Android, #320.

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.
Details there.

Fixes: #320
  • Loading branch information
gnprice committed Nov 1, 2023
1 parent 783b6a8 commit 9be01bd
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 12 deletions.
52 changes: 50 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,41 @@ 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;
}

// Compare these setup steps to the ones in `main` in lib/main.dart .
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 9be01bd

Please sign in to comment.