Skip to content

Commit

Permalink
notif: Support migration of Android notification channels
Browse files Browse the repository at this point in the history
Needed for zulip#340, when updating notification channels to use a custom
notification sound.
  • Loading branch information
rajveermalviya committed Oct 8, 2024
1 parent 5ea12f1 commit b3a5b2a
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 12 deletions.
35 changes: 29 additions & 6 deletions lib/notifications/display.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ AndroidNotificationHostApi get _androidHost => ZulipBinding.instance.androidNoti

/// Service for configuring our Android "notification channel".
class NotificationChannelManager {
/// The channel ID we use for our one notification channel, which we use for
/// all notifications.
// TODO(launch) check this doesn't match zulip-mobile's current or previous
// channel IDs
@visibleForTesting
static const kChannelId = 'messages-1';

Expand All @@ -36,6 +40,8 @@ class NotificationChannelManager {
static final kVibrationPattern = Int64List.fromList([0, 125, 100, 450]);

/// Create our notification channel, if it doesn't already exist.
///
/// Deletes obsolete channels, if present, from old versions of the app.
//
// NOTE when changing anything here: the changes will not take effect
// for existing installs of the app! That's because we'll have already
Expand All @@ -52,11 +58,28 @@ class NotificationChannelManager {
// 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 {
@visibleForTesting
static Future<void> ensureChannel() async {
// See if our current-version channel already exists; delete any obsolete
// previous channels.
var found = false;
final channels = await _androidHost.getNotificationChannels();
for (final channel in channels) {
assert(channel != null); // TODO(#942)
if (channel!.id == kChannelId) {
found = true;
} else {
await _androidHost.deleteNotificationChannel(channel.id);
}
}

if (found) {
// The channel already exists; nothing to do.
return;
}

// The channel doesn't exist. Create it.

await _androidHost.createNotificationChannel(NotificationChannel(
id: kChannelId,
name: 'Messages', // TODO(i18n)
Expand All @@ -81,7 +104,7 @@ class NotificationDisplayManager {
if (launchDetails?.didNotificationLaunchApp ?? false) {
_handleNotificationAppLaunch(launchDetails!.notificationResponse);
}
await NotificationChannelManager._ensureChannel();
await NotificationChannelManager.ensureChannel();
}

static void onFcmMessage(FcmMessage data, Map<String, dynamic> dataJson) {
Expand Down
24 changes: 18 additions & 6 deletions test/model/binding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -554,20 +554,32 @@ class FakeAndroidNotificationHostApi implements AndroidNotificationHostApi {
}
List<NotificationChannel> _createdChannels = [];

/// Consumes the log of calls made to [getNotificationChannels],
/// [deleteNotificationChannel] and [createNotificationChannel].
///
/// Returns a list of function names in the order they were invoked.
List<String> takeChannelMethodCallLogs() {
final result = _channelMethodCallLogs;
_channelMethodCallLogs = [];
return result;
}
List<String> _channelMethodCallLogs = [];

@override
Future<List<NotificationChannel?>> getNotificationChannels() {
// TODO: implement getNotificationChannels
throw UnimplementedError();
Future<List<NotificationChannel?>> getNotificationChannels() async {
_channelMethodCallLogs.add('getNotificationChannels');
return _createdChannels.toList(growable: false);
}

@override
Future<void> deleteNotificationChannel(String channelId) {
// TODO: implement deleteNotificationChannel
throw UnimplementedError();
Future<void> deleteNotificationChannel(String channelId) async {
_channelMethodCallLogs.add('deleteNotificationChannel');
_createdChannels.removeWhere((e) => e.id == channelId);
}

@override
Future<void> createNotificationChannel(NotificationChannel channel) async {
_channelMethodCallLogs.add('createNotificationChannel');
_createdChannels.add(channel);
}

Expand Down
53 changes: 53 additions & 0 deletions test/notifications/display_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,59 @@ void main() {
NotificationChannelManager.kVibrationPattern)
;
});

test('channel is not recreated if one with same id already exists', () async {
await NotificationChannelManager.ensureChannel();
check(testBinding.androidNotificationHost.takeChannelMethodCallLogs())
.deepEquals(['getNotificationChannels', 'createNotificationChannel']);

await NotificationChannelManager.ensureChannel();
check(testBinding.androidNotificationHost.takeChannelMethodCallLogs())
.deepEquals(['getNotificationChannels']);

check(testBinding.androidNotificationHost.takeCreatedChannels()).single
..id.equals(NotificationChannelManager.kChannelId)
..name.equals('Messages')
..importance.equals(NotificationImportance.high)
..lightsEnabled.equals(true)
..vibrationPattern.isNotNull().deepEquals(
NotificationChannelManager.kVibrationPattern);
});

test('obsolete channels are removed', () async {
await NotificationChannelManager.ensureChannel();
check(testBinding.androidNotificationHost.takeChannelMethodCallLogs())
.deepEquals(['getNotificationChannels', 'createNotificationChannel']);

await testBinding.androidNotificationHost.createNotificationChannel(NotificationChannel(
id: 'obsolete-1',
name: 'Obsolete 1',
importance: NotificationImportance.high,
lightsEnabled: true,
vibrationPattern: NotificationChannelManager.kVibrationPattern));
await testBinding.androidNotificationHost.createNotificationChannel(NotificationChannel(
id: 'obsolete-2',
name: 'Obsolete 2',
importance: NotificationImportance.high,
lightsEnabled: true,
vibrationPattern: NotificationChannelManager.kVibrationPattern));
check(testBinding.androidNotificationHost.takeChannelMethodCallLogs())
.deepEquals(['createNotificationChannel', 'createNotificationChannel']);

await NotificationChannelManager.ensureChannel();
check(testBinding.androidNotificationHost.takeChannelMethodCallLogs())
.deepEquals([
'getNotificationChannels',
'deleteNotificationChannel',
'deleteNotificationChannel']);
check(testBinding.androidNotificationHost.takeCreatedChannels()).single
..id.equals(NotificationChannelManager.kChannelId)
..name.equals('Messages')
..importance.equals(NotificationImportance.high)
..lightsEnabled.equals(true)
..vibrationPattern.isNotNull().deepEquals(
NotificationChannelManager.kVibrationPattern);
});
});

group('NotificationDisplayManager show', () {
Expand Down

0 comments on commit b3a5b2a

Please sign in to comment.