@@ -23,14 +23,57 @@ import '../widgets/theme.dart';
23
23
24
24
AndroidNotificationHostApi get _androidHost => ZulipBinding .instance.androidNotificationHost;
25
25
26
+ /// Generates an Android resource uri for the given resource name and type.
27
+ ///
28
+ /// For example, for a resource `@raw/chime3` , where `raw` would be the
29
+ /// resource type and `chime3` would be the resource name it generates the
30
+ /// following uri:
31
+ /// `android.resource://com.zulip.flutter/raw/chime3`
32
+ ///
33
+ /// Based on: https://stackoverflow.com/a/38340580
34
+ Uri resourceUriFromName ({
35
+ required String resourceTypeName,
36
+ required String resourceEntryName,
37
+ }) {
38
+ const packageName = 'com.zulip.flutter' ; // TODO(#407)
39
+
40
+ // Uri scheme for Android resource url.
41
+ // See: https://developer.android.com/reference/android/content/ContentResolver#SCHEME_ANDROID_RESOURCE
42
+ const schemeAndroidResource = 'android.resource' ;
43
+
44
+ return Uri (
45
+ scheme: schemeAndroidResource,
46
+ host: packageName,
47
+ pathSegments: < String > [resourceTypeName, resourceEntryName],
48
+ );
49
+ }
50
+
51
+ enum NotificationSound {
52
+ // Any new entry here must appear in `keep.xml` too, see #528.
53
+ chime2 (resourceName: 'chime2' , fileDisplayName: 'Zulip - Low Chime.m4a' ),
54
+ chime3 (resourceName: 'chime3' , fileDisplayName: 'Zulip - Chime.m4a' ),
55
+ chime4 (resourceName: 'chime4' , fileDisplayName: 'Zulip - High Chime.m4a' );
56
+
57
+ const NotificationSound ({
58
+ required this .resourceName,
59
+ required this .fileDisplayName,
60
+ });
61
+ final String resourceName;
62
+ final String fileDisplayName;
63
+ }
64
+
26
65
/// Service for configuring our Android "notification channel".
27
66
class NotificationChannelManager {
28
67
/// The channel ID we use for our one notification channel, which we use for
29
68
/// all notifications.
30
69
// TODO(launch) check this doesn't match zulip-mobile's current or previous
31
70
// channel IDs
71
+ // Previous values: 'messages-1'
72
+ @visibleForTesting
73
+ static const kChannelId = 'messages-2' ;
74
+
32
75
@visibleForTesting
33
- static const kChannelId = 'messages-1' ;
76
+ static const kDefaultNotificationSound = NotificationSound .chime3 ;
34
77
35
78
/// The vibration pattern we set for notifications.
36
79
// We try to set a vibration pattern that, with the phone in one's pocket,
@@ -39,6 +82,79 @@ class NotificationChannelManager {
39
82
@visibleForTesting
40
83
static final kVibrationPattern = Int64List .fromList ([0 , 125 , 100 , 450 ]);
41
84
85
+ /// Prepare our notification sounds; return a URL for our default sound.
86
+ ///
87
+ /// Where possible, this copies each of our notification sounds into shared storage
88
+ /// so that the user can choose between them in the system notification settings.
89
+ ///
90
+ /// Returns a URL for our default notification sound: either in shared storage
91
+ /// if we successfully copied it there, or else as our internal resource file.
92
+ static Future <String > _ensureInitNotificationSounds () async {
93
+ String defaultSoundUrl = resourceUriFromName (
94
+ resourceTypeName: 'raw' ,
95
+ resourceEntryName: kDefaultNotificationSound.resourceName).toString ();
96
+
97
+ final shouldUseResourceFile = switch (await ZulipBinding .instance.deviceInfo) {
98
+ // Before Android 10 Q, we don't attempt to put the sounds in shared media storage.
99
+ // Just use the resource file directly.
100
+ // TODO(android-sdk-29): Simplify this away.
101
+ AndroidDeviceInfo (: var sdkInt) => sdkInt <= 28 ,
102
+ _ => true ,
103
+ };
104
+ if (shouldUseResourceFile) return defaultSoundUrl;
105
+
106
+ // First, look to see what notification sounds we've already stored,
107
+ // and check against our list of sounds we have.
108
+
109
+ final soundsToAdd = NotificationSound .values.toList ();
110
+ final storedSounds = await _androidHost.listStoredSoundsInNotificationsDirectory ();
111
+ for (final storedSound in storedSounds) {
112
+ assert (storedSound != null ); // TODO(#942)
113
+
114
+ // If the file is one we put there, and has the name we give to our
115
+ // default sound, then use it as the default sound.
116
+ if (storedSound! .fileName == kDefaultNotificationSound.fileDisplayName
117
+ && storedSound.isOwner) {
118
+ defaultSoundUrl = storedSound.uri;
119
+ }
120
+
121
+ // If it has the name of any of our sounds, then don't try to add
122
+ // that sound. This applies even if we didn't put it there: the
123
+ // name is taken, so if we tried adding it anyway it'd get some
124
+ // other name (like "Zulip - Chime (1).m4a", with " (1)" added).
125
+ // Which means the *next* launch would try to add it again ad infinitum.
126
+ // We could avoid this given some other way to uniquely identify the
127
+ // file, but haven't found an obvious one.
128
+ //
129
+ // This does mean it's possible the file isn't the one we would have
130
+ // put there... but it probably is, just from a debug vs. release build
131
+ // of the app (because those have different package names). And anyway,
132
+ // this is a file we're supplying for the user in case they want it, not
133
+ // something where the app depends on it having specific content.
134
+ soundsToAdd.removeWhere ((v) => v.fileDisplayName == storedSound.fileName);
135
+ }
136
+
137
+ // If that leaves any sounds we haven't yet put into shared storage
138
+ // (e.g., because this is the first run after install, or after an
139
+ // upgrade that added a sound), then store those.
140
+
141
+ for (final sound in soundsToAdd) {
142
+ try {
143
+ final url = await _androidHost.copySoundResourceToMediaStore (
144
+ targetFileDisplayName: sound.fileDisplayName,
145
+ sourceResourceName: sound.resourceName);
146
+
147
+ if (sound == kDefaultNotificationSound) {
148
+ defaultSoundUrl = url;
149
+ }
150
+ } catch (e, st) {
151
+ assert (debugLog ("$e \n $st " )); // TODO(log)
152
+ }
153
+ }
154
+
155
+ return defaultSoundUrl;
156
+ }
157
+
42
158
/// Create our notification channel, if it doesn't already exist.
43
159
///
44
160
/// Deletes obsolete channels, if present, from old versions of the app.
@@ -80,13 +196,15 @@ class NotificationChannelManager {
80
196
81
197
// The channel doesn't exist. Create it.
82
198
199
+ final defaultSoundUrl = await _ensureInitNotificationSounds ();
200
+
83
201
await _androidHost.createNotificationChannel (NotificationChannel (
84
202
id: kChannelId,
85
203
name: 'Messages' , // TODO(i18n)
86
204
importance: NotificationImportance .high,
87
205
lightsEnabled: true ,
206
+ soundUri: defaultSoundUrl,
88
207
vibrationPattern: kVibrationPattern,
89
- // TODO(#340) sound
90
208
));
91
209
}
92
210
}
0 commit comments