From 687e5eb518473916c86631f738c05cf2d9ad2058 Mon Sep 17 00:00:00 2001 From: Rajesh Malviya Date: Thu, 10 Oct 2024 01:52:52 +0530 Subject: [PATCH] notif: Use Zulip's distinct notification sound on Android Fixes: #340 --- android/app/src/main/res/raw/chime2.m4a | Bin 0 -> 8830 bytes android/app/src/main/res/raw/chime3.m4a | Bin 0 -> 8474 bytes android/app/src/main/res/raw/chime4.m4a | Bin 0 -> 8870 bytes android/app/src/main/res/raw/keep.xml | 2 +- lib/notifications/display.dart | 128 +++++++++++++++++++++++- test/notifications/display_test.dart | 125 ++++++++++++++++++++++- 6 files changed, 250 insertions(+), 5 deletions(-) create mode 100644 android/app/src/main/res/raw/chime2.m4a create mode 100644 android/app/src/main/res/raw/chime3.m4a create mode 100644 android/app/src/main/res/raw/chime4.m4a diff --git a/android/app/src/main/res/raw/chime2.m4a b/android/app/src/main/res/raw/chime2.m4a new file mode 100644 index 0000000000000000000000000000000000000000..77824b1670c79dc54627409e84f2fcd27a3c9c6d GIT binary patch literal 8830 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 N|3RX?0sVpGe*m$rcUk}d literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/raw/chime3.m4a b/android/app/src/main/res/raw/chime3.m4a new file mode 100644 index 0000000000000000000000000000000000000000..271a9858cb43520087d8d663c258f5c7a6695d43 GIT binary patch literal 8474 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/raw/keep.xml b/android/app/src/main/res/raw/keep.xml index 2a75152d2e..6d3e4d56c6 100644 --- a/android/app/src/main/res/raw/keep.xml +++ b/android/app/src/main/res/raw/keep.xml @@ -12,5 +12,5 @@ https://github.com/zulip/zulip-flutter/issues/528 --> diff --git a/lib/notifications/display.dart b/lib/notifications/display.dart index 64b183e6dd..88f715ae44 100644 --- a/lib/notifications/display.dart +++ b/lib/notifications/display.dart @@ -23,14 +23,32 @@ import '../widgets/theme.dart'; AndroidNotificationHostApi get _androidHost => ZulipBinding.instance.androidNotificationHost; +enum NotificationSound { + // Any new entry here must appear in `keep.xml` too, see #528. + chime2(resourceName: 'chime2', fileDisplayName: 'Zulip - Low Chime.m4a'), + chime3(resourceName: 'chime3', fileDisplayName: 'Zulip - Chime.m4a'), + chime4(resourceName: 'chime4', fileDisplayName: 'Zulip - High Chime.m4a'); + + const NotificationSound({ + required this.resourceName, + required this.fileDisplayName, + }); + final String resourceName; + final String fileDisplayName; +} + /// 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 + // Previous values: 'messages-1' + @visibleForTesting + static const kChannelId = 'messages-2'; + @visibleForTesting - static const kChannelId = 'messages-1'; + static const kDefaultNotificationSound = NotificationSound.chime3; /// The vibration pattern we set for notifications. // We try to set a vibration pattern that, with the phone in one's pocket, @@ -39,6 +57,110 @@ class NotificationChannelManager { @visibleForTesting static final kVibrationPattern = Int64List.fromList([0, 125, 100, 450]); + /// Generates an Android resource URL for the given resource name and type. + /// + /// For example, for a resource `@raw/chime3`, where `raw` would be the + /// resource type and `chime3` would be the resource name it generates the + /// following URL: + /// `android.resource://com.zulip.flutter/raw/chime3` + /// + /// Based on: https://stackoverflow.com/a/38340580 + static Uri _resourceUrlFromName({ + required String resourceTypeName, + required String resourceEntryName, + }) { + const packageName = 'com.zulip.flutter'; // TODO(#407) + + // URL scheme for Android resource url. + // See: https://developer.android.com/reference/android/content/ContentResolver#SCHEME_ANDROID_RESOURCE + const schemeAndroidResource = 'android.resource'; + + return Uri( + scheme: schemeAndroidResource, + host: packageName, + pathSegments: [resourceTypeName, resourceEntryName], + ); + } + + /// Prepare our notification sounds; return a URL for our default sound. + /// + /// Where possible, this copies each of our notification sounds into shared storage + /// so that the user can choose between them in the system notification settings. + /// + /// Returns a URL for our default notification sound: either in shared storage + /// if we successfully copied it there, or else as our internal resource file. + static Future _ensureInitNotificationSounds() async { + String defaultSoundUrl = _resourceUrlFromName( + resourceTypeName: 'raw', + resourceEntryName: kDefaultNotificationSound.resourceName).toString(); + + final shouldUseResourceFile = switch (await ZulipBinding.instance.deviceInfo) { + // Before Android 10 Q, we don't attempt to put the sounds in shared media storage. + // Just use the resource file directly. + // TODO(android-sdk-29): Simplify this away. + AndroidDeviceInfo(:var sdkInt) => sdkInt < 29, + _ => true, + }; + if (shouldUseResourceFile) return defaultSoundUrl; + + // First, look to see what notification sounds we've already stored, + // and check against our list of sounds we have. + final soundsToAdd = NotificationSound.values.toList(); + + final List storedSounds; + try { + storedSounds = await _androidHost.listStoredSoundsInNotificationsDirectory(); + } catch (e, st) { + assert(debugLog('$e\n$st')); // TODO(log) + return defaultSoundUrl; + } + for (final storedSound in storedSounds) { + assert(storedSound != null); // TODO(#942) + + // If the file is one we put there, and has the name we give to our + // default sound, then use it as the default sound. + if (storedSound!.fileName == kDefaultNotificationSound.fileDisplayName + && storedSound.isOwned) { + defaultSoundUrl = storedSound.contentUrl; + } + + // If it has the name of any of our sounds, then don't try to add + // that sound. This applies even if we didn't put it there: the + // name is taken, so if we tried adding it anyway it'd get some + // other name (like "Zulip - Chime (1).m4a", with " (1)" added). + // Which means the *next* launch would try to add it again ad infinitum. + // We could avoid this given some other way to uniquely identify the + // file, but haven't found an obvious one. + // + // This does mean it's possible the file isn't the one we would have + // put there... but it probably is, just from a debug vs. release build + // of the app (because those may have different package names). And anyway, + // this is a file we're supplying for the user in case they want it, not + // something where the app depends on it having specific content. + soundsToAdd.removeWhere((v) => v.fileDisplayName == storedSound.fileName); + } + + // If that leaves any sounds we haven't yet put into shared storage + // (e.g., because this is the first run after install, or after an + // upgrade that added a sound), then store those. + + for (final sound in soundsToAdd) { + try { + final url = await _androidHost.copySoundResourceToMediaStore( + targetFileDisplayName: sound.fileDisplayName, + sourceResourceName: sound.resourceName); + + if (sound == kDefaultNotificationSound) { + defaultSoundUrl = url; + } + } catch (e, st) { + assert(debugLog("$e\n$st")); // TODO(log) + } + } + + return defaultSoundUrl; + } + /// Create our notification channel, if it doesn't already exist. /// /// Deletes obsolete channels, if present, from old versions of the app. @@ -80,13 +202,15 @@ class NotificationChannelManager { // The channel doesn't exist. Create it. + final defaultSoundUrl = await _ensureInitNotificationSounds(); + await _androidHost.createNotificationChannel(NotificationChannel( id: kChannelId, name: 'Messages', // TODO(i18n) importance: NotificationImportance.high, lightsEnabled: true, + soundUrl: defaultSoundUrl, vibrationPattern: kVibrationPattern, - // TODO(#340) sound )); } } diff --git a/test/notifications/display_test.dart b/test/notifications/display_test.dart index 88aee2d41d..23b6fd8357 100644 --- a/test/notifications/display_test.dart +++ b/test/notifications/display_test.dart @@ -13,6 +13,7 @@ import 'package:http/testing.dart' as http_testing; import 'package:zulip/api/model/model.dart'; import 'package:zulip/api/notifications.dart'; import 'package:zulip/host/android_notifications.dart'; +import 'package:zulip/model/binding.dart'; import 'package:zulip/model/localizations.dart'; import 'package:zulip/model/narrow.dart'; import 'package:zulip/model/store.dart'; @@ -121,7 +122,7 @@ void main() { await NotificationService.instance.start(); } - group('NotificationChannelManager', () { + group('NotificationChannelManager create channel', () { test('smoke', () async { await init(); check(testBinding.androidNotificationHost.takeCreatedChannels()).single @@ -129,7 +130,8 @@ void main() { ..name.equals('Messages') ..importance.equals(NotificationImportance.high) ..lightsEnabled.equals(true) - ..soundUrl.isNull() + ..soundUrl.equals(testBinding.androidNotificationHost.fakeStoredNotificationSoundUrl( + NotificationChannelManager.kDefaultNotificationSound.resourceName)) ..vibrationPattern.isNotNull().deepEquals( NotificationChannelManager.kVibrationPattern) ; @@ -211,6 +213,120 @@ void main() { }); }); + group('NotificationChannelManager sounds', () { + final defaultSoundResourceName = + NotificationChannelManager.kDefaultNotificationSound.resourceName; + String fakeStoredUrl(String resourceName) => + testBinding.androidNotificationHost.fakeStoredNotificationSoundUrl(resourceName); + String fakeResourceUrl(String resourceName) => + 'android.resource://com.zulip.flutter/raw/$resourceName'; + + test('on Android 28 (and lower) resource file is used for notification sound', () async { + addTearDown(testBinding.reset); + final androidNotificationHost = testBinding.androidNotificationHost; + + testBinding.deviceInfoResult = + const AndroidDeviceInfo(sdkInt: 28, release: '9'); + + // Ensure that on Android 10, notification sounds aren't being copied to + // the media store, and resource file is used directly. + await NotificationChannelManager.ensureChannel(); + check(androidNotificationHost.takeCopySoundResourceToMediaStoreCalls()) + .isEmpty(); + check(androidNotificationHost.takeCreatedChannels()) + .single + .soundUrl.equals(fakeResourceUrl(defaultSoundResourceName)); + }); + + test('notification sound resource files are being copied to the media store', () async { + addTearDown(testBinding.reset); + final androidNotificationHost = testBinding.androidNotificationHost; + + await NotificationChannelManager.ensureChannel(); + check(androidNotificationHost.takeCopySoundResourceToMediaStoreCalls()) + .deepEquals(NotificationSound.values.map((e) => ( + sourceResourceName: e.resourceName, + targetFileDisplayName: e.fileDisplayName), + )); + + // Ensure the default source URL points to a file in the media store, + // rather than a resource file. + check(androidNotificationHost.takeCreatedChannels()) + .single + .soundUrl.equals(fakeStoredUrl(defaultSoundResourceName)); + }); + + test('notification sounds are not copied again if they were previously copied', () async { + addTearDown(testBinding.reset); + final androidNotificationHost = testBinding.androidNotificationHost; + + // Emulate that all notifications sounds are already in the media store. + androidNotificationHost.setupStoredNotificationSounds( + NotificationSound.values.map((e) => StoredNotificationSound( + fileName: e.fileDisplayName, + isOwned: true, + contentUrl: fakeStoredUrl(e.resourceName)), + ).toList(), + ); + + await NotificationChannelManager.ensureChannel(); + check(androidNotificationHost.takeCopySoundResourceToMediaStoreCalls()) + .isEmpty(); + check(androidNotificationHost.takeCreatedChannels()) + .single + .soundUrl.equals(fakeStoredUrl(defaultSoundResourceName)); + }); + + test('new notification sounds are copied to media store', () async { + addTearDown(testBinding.reset); + final androidNotificationHost = testBinding.androidNotificationHost; + + // Emulate that except one sound, all other sounds are already in + // media store. + androidNotificationHost.setupStoredNotificationSounds( + NotificationSound.values.skip(1).map((e) => StoredNotificationSound( + fileName: e.fileDisplayName, + isOwned: true, + contentUrl: fakeStoredUrl(e.resourceName)), + ).toList() + ); + + await NotificationChannelManager.ensureChannel(); + final firstSound = NotificationSound.values.first; + check(androidNotificationHost.takeCopySoundResourceToMediaStoreCalls()) + .single + ..sourceResourceName.equals(firstSound.resourceName) + ..targetFileDisplayName.equals(firstSound.fileDisplayName); + check(androidNotificationHost.takeCreatedChannels()) + .single + .soundUrl.equals(fakeStoredUrl(defaultSoundResourceName)); + }); + + test('no recopying of existing notification sounds in the media store; default sound URL points to resource file', () async { + addTearDown(testBinding.reset); + final androidNotificationHost = testBinding.androidNotificationHost; + + androidNotificationHost.setupStoredNotificationSounds( + NotificationSound.values.map((e) => StoredNotificationSound( + fileName: e.fileDisplayName, + isOwned: false, + contentUrl: fakeStoredUrl(e.resourceName)), + ).toList() + ); + + // Ensure that if a notification sound with the same name already exists + // in the media store, but it wasn't copied by us, no recopying should + // happen. Additionally, the default sound URL should point to the + // resource file, not the version in the media store. + await NotificationChannelManager.ensureChannel(); + check(androidNotificationHost.takeCopySoundResourceToMediaStoreCalls()) + .isEmpty(); + check(androidNotificationHost.takeCreatedChannels()) + .single + .soundUrl.equals(fakeResourceUrl(defaultSoundResourceName)); + }); + }); + group('NotificationDisplayManager show', () { void checkNotification(MessageFcmMessage data, { required List messageStyleMessages, @@ -1182,6 +1298,11 @@ void main() { }); } +extension on Subject { + Subject get targetFileDisplayName => has((x) => x.targetFileDisplayName, 'targetFileDisplayName'); + Subject get sourceResourceName => has((x) => x.sourceResourceName, 'sourceResourceName'); +} + extension NotificationChannelChecks on Subject { Subject get id => has((x) => x.id, 'id'); Subject get importance => has((x) => x.importance, 'importance');