-
Notifications
You must be signed in to change notification settings - Fork 308
Show notifications! on Android, in foreground #344
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,10 @@ | ||
import 'package:flutter/foundation.dart'; | ||
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; | ||
|
||
import 'api/notifications.dart'; | ||
import 'log.dart'; | ||
import 'model/binding.dart'; | ||
import 'widgets/app.dart'; | ||
|
||
class NotificationService { | ||
static NotificationService get instance => (_instance ??= NotificationService._()); | ||
|
@@ -38,6 +40,7 @@ class NotificationService { | |
// TODO(#324) defer notif setup if user not logged into any accounts | ||
// (in order to avoid calling for permissions) | ||
|
||
await NotificationDisplayManager._init(); | ||
ZulipBinding.instance.firebaseMessagingOnMessage.listen(_onRemoteMessage); | ||
|
||
// Get the FCM registration token, now and upon changes. See FCM API docs: | ||
|
@@ -71,9 +74,121 @@ class NotificationService { | |
static void _onRemoteMessage(FirebaseRemoteMessage message) { | ||
assert(debugLog("notif message: ${message.data}")); | ||
final data = FcmMessage.fromJson(message.data); | ||
if (data is MessageFcmMessage) { | ||
assert(debugLog('notif message content: ${data.content}')); | ||
// TODO(#122): show notification UI | ||
switch (data) { | ||
case MessageFcmMessage(): NotificationDisplayManager._onMessageFcmMessage(data, message.data); | ||
case RemoveFcmMessage(): break; // TODO(#341) handle | ||
case UnexpectedFcmMessage(): break; // TODO(log) | ||
} | ||
} | ||
} | ||
|
||
/// Service for configuring our Android "notification channel". | ||
class NotificationChannelManager { | ||
@visibleForTesting | ||
static const kChannelId = 'messages-1'; | ||
|
||
/// The vibration pattern we set for notifications. | ||
// We try to set a vibration pattern that, with the phone in one's pocket, | ||
// is both distinctly present and distinctly different from the default. | ||
// Discussion: https://chat.zulip.org/#narrow/stream/48-mobile/topic/notification.20vibration.20pattern/near/1284530 | ||
@visibleForTesting | ||
static final kVibrationPattern = Int64List.fromList([0, 125, 100, 450]); | ||
|
||
/// Create our notification channel, if it doesn't already exist. | ||
// | ||
// NOTE when changing anything here: the changes will not take effect | ||
// for existing installs of the app! That's because we'll have already | ||
// created the channel with the old settings, and they're in the user's | ||
// hands from there. Our choices are: | ||
// | ||
// * Leave the old settings in place for existing installs, so the | ||
// changes only apply to new installs. | ||
// | ||
// * Change `kChannelId`, so that we abandon the old channel and use | ||
// a new one. Existing installs will get the new settings. | ||
// | ||
// This also means that if the user has changed any of the notification | ||
// settings for the channel -- like "override Do Not Disturb", or "use | ||
// a different sound", or "don't pop on screen" -- their changes get | ||
// reset. So this has to be done sparingly. | ||
// | ||
// If we do this, we should also look for any channel with the old | ||
// channel ID and delete it. See zulip-mobile's `createNotificationChannel` | ||
// in android/app/src/main/java/com/zulipmobile/notifications/NotificationChannelManager.kt . | ||
static Future<void> _ensureChannel() async { | ||
final plugin = ZulipBinding.instance.notifications; | ||
await plugin.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>() | ||
?.createNotificationChannel(AndroidNotificationChannel( | ||
kChannelId, | ||
'Messages', // TODO(i18n) | ||
importance: Importance.high, | ||
enableLights: true, | ||
vibrationPattern: kVibrationPattern, | ||
// TODO(#340) sound | ||
)); | ||
} | ||
} | ||
|
||
/// Service for managing the notifications shown to the user. | ||
class NotificationDisplayManager { | ||
// We rely on the tag instead. | ||
@visibleForTesting | ||
static const kNotificationId = 0; | ||
|
||
static Future<void> _init() async { | ||
await ZulipBinding.instance.notifications.initialize( | ||
const InitializationSettings( | ||
android: AndroidInitializationSettings('zulip_notification'), | ||
), | ||
); | ||
await NotificationChannelManager._ensureChannel(); | ||
} | ||
|
||
static void _onMessageFcmMessage(MessageFcmMessage data, Map<String, dynamic> dataJson) { | ||
assert(debugLog('notif message content: ${data.content}')); | ||
final title = switch (data.recipient) { | ||
FcmMessageStreamRecipient(:var streamName?, :var topic) => | ||
'$streamName > $topic', | ||
FcmMessageStreamRecipient(:var topic) => | ||
'(unknown stream) > $topic', // TODO get stream name from data | ||
FcmMessageDmRecipient(:var allRecipientIds) when allRecipientIds.length > 2 => | ||
'${data.senderFullName} to you and ${allRecipientIds.length - 2} others', // TODO(i18n), also plural; TODO use others' names, from data | ||
FcmMessageDmRecipient() => | ||
data.senderFullName, | ||
}; | ||
ZulipBinding.instance.notifications.show( | ||
kNotificationId, | ||
title, | ||
data.content, | ||
NotificationDetails(android: AndroidNotificationDetails( | ||
NotificationChannelManager.kChannelId, | ||
// This [FlutterLocalNotificationsPlugin.show] call can potentially create | ||
// a new channel, if our channel doesn't already exist. That *shouldn't* | ||
// happen; if it does, it won't get the right settings. Set the channel | ||
// name in that case to something that has a chance of warning the user, | ||
// and that can serve as a signature to diagnose the situation in support. | ||
// But really we should fix flutter_local_notifications to not do that | ||
// (see issue linked below), or replace that package entirely (#351). | ||
'(Zulip internal error)', // TODO never implicitly create channel: https://github.com/MaikuB/flutter_local_notifications/issues/2135 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, it looks like "(Zulip internal error)" is passed as a …Oh I guess the point is we don't want to pass any channel name at all, because we don't want this call to create a channel at all (I see the issue you filed). But we have to choose a name, so we choose this one. And maybe if a channel is created, the string will be presented to the user somewhere, like in the system notification settings for the app? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, exactly. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added a comment explaining this more. |
||
tag: _conversationKey(data), | ||
color: kZulipBrandColor, | ||
icon: 'zulip_notification', // TODO vary for debug | ||
// TODO(#128) inbox-style | ||
))); | ||
} | ||
|
||
static String _conversationKey(MessageFcmMessage data) { | ||
final groupKey = _groupKey(data); | ||
final conversation = switch (data.recipient) { | ||
FcmMessageStreamRecipient(:var streamId, :var topic) => 'stream:$streamId:$topic', | ||
FcmMessageDmRecipient(:var allRecipientIds) => 'dm:${allRecipientIds.join(',')}', | ||
}; | ||
return '$groupKey|$conversation'; | ||
} | ||
|
||
static String _groupKey(FcmMessageWithIdentity data) { | ||
// The realm URL can't contain a `|`, because `|` is not a URL code point: | ||
// https://url.spec.whatwg.org/#url-code-points | ||
return "${data.realmUri}|${data.userId}"; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm. We don't have any local notifications to show, only remote ones. In my understanding (mostly informed by iOS), local notifications are for things like timers that the app sets independently of any messages received from a server. Evidently this has APIs we can use for our remote notifications, but it might also have lots of baggage for functionality we'll never need.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, good question. Replied in chat here: https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/notification.20libraries/near/1670983
In short: indeed it does have such baggage, and we'll likely want to move to writing our own thin layer with Pigeon in the future, but this library is what's recommended by Flutter upstream and seems to be the best existing thing to use.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Filed that thought as #351.