Skip to content

Commit

Permalink
notif: Switch to our own Pigeon-based plugin for showing a notification
Browse files Browse the repository at this point in the history
This delivers the first major tranche of #351, switching from
package:flutter_local_notifications to our own thin API binding
based on Pigeon.  With this commit, we've converted over one of
the handful of different methods we use from that package,
and it's the most complex of them by a substantial margin.

Fixes-partly: #351
  • Loading branch information
gnprice committed Mar 28, 2024
1 parent 2a37606 commit d412a04
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 43 deletions.
71 changes: 37 additions & 34 deletions lib/notifications/display.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:flutter/widgets.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';

import '../api/notifications.dart';
import '../host/android_notifications.dart';
import '../log.dart';
import '../model/binding.dart';
import '../model/narrow.dart';
Expand Down Expand Up @@ -99,40 +100,42 @@ class NotificationDisplayManager {
data.senderFullName,
};
final conversationKey = _conversationKey(data);
ZulipBinding.instance.notifications.show(
// When creating the PendingIntent for the user to open the notification,
// the plugin makes the underlying Intent objects look the same.
// They differ in their extras, but that doesn't count:
// https://developer.android.com/reference/android/app/PendingIntent
//
// This leaves only PendingIntent.requestCode to distinguish one
// PendingIntent from another; the plugin sets that to the notification ID.
// We need a distinct PendingIntent for each conversation, so that the
// notifications can lead to the right conversations when opened.
// So, use a hash of the conversation key.
notificationIdAsHashOf(conversationKey),
title,
data.content,
payload: jsonEncode(dataJson),
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
tag: conversationKey,
color: kZulipBrandColor,
// TODO vary notification icon for debug
icon: 'zulip_notification', // This name must appear in keep.xml too: https://github.com/zulip/zulip-flutter/issues/528
// TODO(#128) inbox-style

// TODO plugin sets PendingIntent.FLAG_UPDATE_CURRENT; is that OK?
// TODO plugin doesn't set our Intent flags; is that OK?
)));
ZulipBinding.instance.androidNotificationHost.notify(
// TODO the notification ID can be constant, instead of matching requestCode
// (This is a legacy of `flutter_local_notifications`.)
id: notificationIdAsHashOf(conversationKey),
tag: conversationKey,
channelId: NotificationChannelManager.kChannelId,

contentTitle: title,
contentText: data.content,
color: kZulipBrandColor.value,
// TODO vary notification icon for debug
smallIconResourceName: 'zulip_notification', // This name must appear in keep.xml too: https://github.com/zulip/zulip-flutter/issues/528
// TODO(#128) inbox-style

contentIntent: PendingIntent(
// TODO make intent URLs distinct, instead of requestCode
// (This way is a legacy of flutter_local_notifications.)
// The Intent objects we make for different conversations look the same.
// They differ in their extras, but that doesn't count:
// https://developer.android.com/reference/android/app/PendingIntent
//
// This leaves only PendingIntent.requestCode to distinguish one
// PendingIntent from another; the plugin sets that to the notification ID.
// We need a distinct PendingIntent for each conversation, so that the
// notifications can lead to the right conversations when opened.
// So, use a hash of the conversation key.
requestCode: notificationIdAsHashOf(conversationKey),

// TODO is setting PendingIntentFlag.updateCurrent OK?
// (That's a legacy of `flutter_local_notifications`.)
flags: PendingIntentFlag.immutable | PendingIntentFlag.updateCurrent,
intentPayload: jsonEncode(dataJson),
// TODO this doesn't set the Intent flags we set in zulip-mobile; is that OK?
// (This is a legacy of `flutter_local_notifications`.)
),
);
}

/// A notification ID, derived as a hash of the given string key.
Expand Down
42 changes: 33 additions & 9 deletions test/notifications/display_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart' hi
import 'package:flutter_test/flutter_test.dart';
import 'package:zulip/api/model/model.dart';
import 'package:zulip/api/notifications.dart';
import 'package:zulip/host/android_notifications.dart';
import 'package:zulip/model/narrow.dart';
import 'package:zulip/model/store.dart';
import 'package:zulip/notifications/display.dart';
Expand Down Expand Up @@ -111,16 +112,21 @@ void main() {
final expectedTag = '${data.realmUri}|${data.userId}|$expectedTagComponent';
final expectedId =
NotificationDisplayManager.notificationIdAsHashOf(expectedTag);
check(testBinding.notifications.takeShowCalls()).single
const expectedIntentFlags =
PendingIntentFlag.immutable | PendingIntentFlag.updateCurrent;
check(testBinding.androidNotificationHost.takeNotifyCalls()).single
..id.equals(expectedId)
..title.equals(expectedTitle)
..body.equals(data.content)
..payload.equals(jsonEncode(data.toJson()))
..notificationDetails.isNotNull().android.isNotNull().which((it) => it
..channelId.equals(NotificationChannelManager.kChannelId)
..tag.equals(expectedTag)
..color.equals(kZulipBrandColor)
..icon.equals('zulip_notification')
..tag.equals(expectedTag)
..channelId.equals(NotificationChannelManager.kChannelId)
..contentTitle.equals(expectedTitle)
..contentText.equals(data.content)
..color.equals(kZulipBrandColor.value)
..smallIconResourceName.equals('zulip_notification')
..extras.isNull()
..contentIntent.which((it) => it.isNotNull()
..requestCode.equals(expectedId)
..flags.equals(expectedIntentFlags)
..intentPayload.equals(jsonEncode(data.toJson()))
);
}

Expand Down Expand Up @@ -358,6 +364,24 @@ extension ShowCallChecks on Subject<FlutterLocalNotificationsPluginShowCall> {
Subject<String?> get payload => has((x) => x.payload, 'payload');
}

extension on Subject<AndroidNotificationHostApiNotifyCall> {
Subject<String?> get tag => has((x) => x.tag, 'tag');
Subject<int> get id => has((x) => x.id, 'id');
Subject<String> get channelId => has((x) => x.channelId, 'channelId');
Subject<int?> get color => has((x) => x.color, 'color');
Subject<PendingIntent?> get contentIntent => has((x) => x.contentIntent, 'contentIntent');
Subject<String?> get contentText => has((x) => x.contentText, 'contentText');
Subject<String?> get contentTitle => has((x) => x.contentTitle, 'contentTitle');
Subject<Map<String?, String?>?> get extras => has((x) => x.extras, 'extras');
Subject<String?> get smallIconResourceName => has((x) => x.smallIconResourceName, 'smallIconResourceName');
}

extension on Subject<PendingIntent> {
Subject<int> get requestCode => has((x) => x.requestCode, 'requestCode');
Subject<String> get intentPayload => has((x) => x.intentPayload, 'intentPayload');
Subject<int> get flags => has((x) => x.flags, 'flags');
}

extension NotificationDetailsChecks on Subject<NotificationDetails> {
Subject<AndroidNotificationDetails?> get android => has((x) => x.android, 'android');
Subject<DarwinNotificationDetails?> get iOS => has((x) => x.iOS, 'iOS');
Expand Down

0 comments on commit d412a04

Please sign in to comment.