Skip to content

Commit 73bb87b

Browse files
committed
notif: Handle when app in background, too (on Android)
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
1 parent 30b4a85 commit 73bb87b

File tree

2 files changed

+74
-12
lines changed

2 files changed

+74
-12
lines changed

lib/notifications.dart

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,22 @@ class NotificationService {
1919
static void debugReset() {
2020
instance.token.dispose();
2121
_instance = null;
22+
assert(debugBackgroundIsolateIsLive = true);
2223
}
2324

25+
/// Whether a background isolate should initialize [LiveZulipBinding].
26+
///
27+
/// Ordinarily a [ZulipBinding.firebaseMessagingOnBackgroundMessage] callback
28+
/// will be invoked in a background isolate where it must set up its
29+
/// [ZulipBinding], just as the `main` function does for most of the app.
30+
/// Consequently, by default we have that callback initialize
31+
/// [LiveZulipBinding], just like `main` does.
32+
///
33+
/// In a test that behavior is undesirable. Tests that will cause
34+
/// [ZulipBinding.firebaseMessagingOnBackgroundMessage] callbacks
35+
/// to get invoked should therefore set this to false.
36+
static bool debugBackgroundIsolateIsLive = true;
37+
2438
/// The FCM registration token for this install of the app.
2539
///
2640
/// This is unique to the (app, device) pair, but not permanent.
@@ -41,7 +55,8 @@ class NotificationService {
4155
// (in order to avoid calling for permissions)
4256

4357
await NotificationDisplayManager._init();
44-
ZulipBinding.instance.firebaseMessagingOnMessage.listen(_onRemoteMessage);
58+
ZulipBinding.instance.firebaseMessagingOnMessage.listen(_onForegroundMessage);
59+
ZulipBinding.instance.firebaseMessagingOnBackgroundMessage(_onBackgroundMessage);
4560

4661
// Get the FCM registration token, now and upon changes. See FCM API docs:
4762
// https://firebase.google.com/docs/cloud-messaging/android/client#sample-register
@@ -71,8 +86,40 @@ class NotificationService {
7186
token.value = value;
7287
}
7388

74-
static void _onRemoteMessage(FirebaseRemoteMessage message) {
89+
static void _onForegroundMessage(FirebaseRemoteMessage message) {
7590
assert(debugLog("notif message: ${message.data}"));
91+
_onRemoteMessage(message);
92+
}
93+
94+
static Future<void> _onBackgroundMessage(FirebaseRemoteMessage message) async {
95+
// This callback will run in a separate isolate from the rest of the app.
96+
// See docs:
97+
// https://firebase.flutter.dev/docs/messaging/usage/#background-messages
98+
_initBackgroundIsolate();
99+
100+
assert(debugLog("notif message in background: ${message.data}"));
101+
_onRemoteMessage(message);
102+
}
103+
104+
static void _initBackgroundIsolate() {
105+
bool isolateIsLive = true;
106+
assert(() {
107+
isolateIsLive = debugBackgroundIsolateIsLive;
108+
return true;
109+
}());
110+
if (!isolateIsLive) {
111+
return;
112+
}
113+
114+
assert(() {
115+
debugLogEnabled = true;
116+
return true;
117+
}());
118+
LiveZulipBinding.ensureInitialized();
119+
NotificationDisplayManager._init(); // TODO call this just once per isolate
120+
}
121+
122+
static void _onRemoteMessage(FirebaseRemoteMessage message) {
76123
final data = FcmMessage.fromJson(message.data);
77124
switch (data) {
78125
case MessageFcmMessage(): NotificationDisplayManager._onMessageFcmMessage(data, message.data);

test/notifications_test.dart

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ void main() {
6868
addTearDown(testBinding.reset);
6969
testBinding.firebaseMessagingInitialToken = '012abc';
7070
addTearDown(NotificationService.debugReset);
71+
NotificationService.debugBackgroundIsolateIsLive = false;
7172
await NotificationService.instance.start();
7273
}
7374

@@ -93,13 +94,10 @@ void main() {
9394
});
9495

9596
group('NotificationDisplayManager', () {
96-
Future<void> checkNotification(MessageFcmMessage data, {
97+
void checkNotification(MessageFcmMessage data, {
9798
required String expectedTitle,
9899
required String expectedTagComponent,
99-
}) async {
100-
testBinding.firebaseMessaging.onMessage.add(
101-
RemoteMessage(data: data.toJson()));
102-
await null;
100+
}) {
103101
check(testBinding.notifications.takeShowCalls()).single
104102
..id.equals(NotificationDisplayManager.kNotificationId)
105103
..title.equals(expectedTitle)
@@ -112,11 +110,28 @@ void main() {
112110
);
113111
}
114112

113+
Future<void> checkNotifications(MessageFcmMessage data, {
114+
required String expectedTitle,
115+
required String expectedTagComponent,
116+
}) async {
117+
testBinding.firebaseMessaging.onMessage.add(
118+
RemoteMessage(data: data.toJson()));
119+
await null;
120+
checkNotification(data, expectedTitle: expectedTitle,
121+
expectedTagComponent: expectedTagComponent);
122+
123+
testBinding.firebaseMessaging.onBackgroundMessage.add(
124+
RemoteMessage(data: data.toJson()));
125+
await null;
126+
checkNotification(data, expectedTitle: expectedTitle,
127+
expectedTagComponent: expectedTagComponent);
128+
}
129+
115130
test('stream message', () async {
116131
await init();
117132
final stream = eg.stream();
118133
final message = eg.streamMessage(stream: stream);
119-
await checkNotification(messageFcmMessage(message, streamName: stream.name),
134+
await checkNotifications(messageFcmMessage(message, streamName: stream.name),
120135
expectedTitle: '${stream.name} > ${message.subject}',
121136
expectedTagComponent: 'stream:${message.streamId}:${message.subject}');
122137
});
@@ -125,31 +140,31 @@ void main() {
125140
await init();
126141
final stream = eg.stream();
127142
final message = eg.streamMessage(stream: stream);
128-
await checkNotification(messageFcmMessage(message, streamName: null),
143+
await checkNotifications(messageFcmMessage(message, streamName: null),
129144
expectedTitle: '(unknown stream) > ${message.subject}',
130145
expectedTagComponent: 'stream:${message.streamId}:${message.subject}');
131146
});
132147

133148
test('group DM', () async {
134149
await init();
135150
final message = eg.dmMessage(from: eg.thirdUser, to: [eg.otherUser, eg.selfUser]);
136-
await checkNotification(messageFcmMessage(message),
151+
await checkNotifications(messageFcmMessage(message),
137152
expectedTitle: "${eg.thirdUser.fullName} to you and 1 others",
138153
expectedTagComponent: 'dm:${message.allRecipientIds.join(",")}');
139154
});
140155

141156
test('1:1 DM', () async {
142157
await init();
143158
final message = eg.dmMessage(from: eg.otherUser, to: [eg.selfUser]);
144-
await checkNotification(messageFcmMessage(message),
159+
await checkNotifications(messageFcmMessage(message),
145160
expectedTitle: eg.otherUser.fullName,
146161
expectedTagComponent: 'dm:${message.allRecipientIds.join(",")}');
147162
});
148163

149164
test('self-DM', () async {
150165
await init();
151166
final message = eg.dmMessage(from: eg.selfUser, to: []);
152-
await checkNotification(messageFcmMessage(message),
167+
await checkNotifications(messageFcmMessage(message),
153168
expectedTitle: eg.selfUser.fullName,
154169
expectedTagComponent: 'dm:${message.allRecipientIds.join(",")}');
155170
});

0 commit comments

Comments
 (0)