Notification Sounds
Custom notification sounds are "drop the file in and release": put a .wav file in the repo-root sounds/ folder, rebuild, and a push or local notification with that sound plays it. No payload changes, no channel setup in the web app.
Add a Sound
Copy the file to
sounds/<name>.wav, for examplesounds/order_alert.wav.Rebuild and release the app.
Send
sound: "order_alert"(or"order_alert.wav") in the notification data.
Rules, checked by both the Android and the iOS build:
Rule | Build result |
|---|---|
Only | Fails on any other file |
Name: lowercase | Fails on an invalid name |
Not | Fails on a reserved name |
No subfolders | Fails on a subfolder |
Not also present in | Android build fails on the duplicate |
Linear PCM, µ-law or A-law audio (what iOS can play) | iOS build fails on another format or an unreadable file |
30 seconds or shorter | iOS build warns |
README.md and hidden files (such as .gitkeep) in sounds/ are ignored. sounds/sounds.json holds optional channel overrides, see Channel Overrides.
Keep sounds short. iOS plays the default sound for sounds longer than 30 seconds.
Sounds already in res/raw
Sound files placed directly in android/app/src/main/res/raw/ (.wav, .ogg, .mp3, .m4a, .aac, .flac, .opus) count as bundled sounds too: they get a channel and routing exactly like files in sounds/. Existing app branches that ship sounds this way keep working with no changes.
Sounds already in the iOS App target
Sound files added to the App target directly in Xcode (.wav, .caf, .aiff) keep working, with or without the extension in sound. If the same file name is also in sounds/, the sounds/ copy wins and the iOS build warns; keep only one of them.
Channel Overrides (sounds.json)
By default each sound's channel is named after the file (ity_emergency becomes "Ity emergency"), has no description and high importance. An optional sounds/sounds.json overrides these per sound:
Keys are sound names: a file in
sounds/(without.wav) or a sound inres/raw.Every field is optional. Sounds without an entry, and fields left out, keep the defaults. No
sounds.jsonat all means defaults for every sound.name: non-empty string shown in the system notification settings.description: string shown under the channel name in the system settings.importance:min,low,default,highormax(any case), or the number1to5. These are the valuesAndroidNotification.createNotificationChannelaccepts (none/0is not allowed, it would create a blocked channel).
The build fails on invalid JSON (strict: no comments, trailing commas or duplicate keys), a root that is not an object, an unknown key, an invalid importance, a name that is empty, or an entry for a sound that is not bundled.
Removing an entry (or a field) restores the default name and clears the description on the next app start.
How It Works (Android)
On Android 8+ the channel's sound plays, not the sound set on the notification. A custom sound set on a notification posted to default_channel is ignored. So the app gives each bundled sound its own channel.
Build
A Gradle task (generate<Variant>NotificationSounds, logic in android/buildSrc/) runs before resources are merged:
copies
sounds/*.wavinto a generated res directory (app/build/generated/res/...), registered as an extra res source. Nothing is written intosrc/main/res;writes keep rules (
tools:keep) for every bundled sound, including those inres/raw, so release resource shrinking never removes them;writes the list of bundled sound names (
R.array.mobiweb_bundled_sounds), which the app reads at runtime, plus parallel arrays with thesounds.jsonchannel name, description and importance.
Channels
Wherever default_channel is created (app start and before each notification is shown), the app also:
creates one channel per bundled sound, id
mobiweb_sound_<name>, with that sound, high importance, and a name derived from the file name (ity_emergencybecomes "Ity emergency"), unlesssounds.jsonoverrides them. On an existing channel only the name and description are updated;deletes channels whose id starts with
mobiweb_sound_and whose sound is no longer bundled.
No other channel is ever touched: default_channel and channels created by the web app (AndroidNotification.createNotificationChannel) are left alone.
Removing a sound deletes its channel on the next app start (or the next notification). Users lose any per-channel settings they changed for it. If the same sound is added back later, Android restores the old channel together with the user's settings for it.
Channel ids beginning with mobiweb_sound_ are reserved for these auto channels. Don't create web channels with that prefix: they would be deleted.
Routing
A notification (push or local) is posted on mobiweb_sound_<name> only when all of these hold:
soundis custom: not empty, notdefault, notnone;no
channel_idwas given (neither indatanor in the FCM notification block);the notification is not
silent;the sound name, with any path and extension stripped, is a bundled sound (
ity_emergency,ity_emergency.wavandandroid.resource://.../raw/ity_emergencyall match).
In every other case the notification renders exactly as before:
|
| Channel used | Sound |
|---|---|---|---|
| none |
|
|
| none |
|
|
|
|
| Whatever |
| none |
| Channel default (as before) |
| none |
| System default |
| none |
| As before |
a URI (not a bundled name) | none |
| As before (URI fallback) |
Below Android 8 there are no channels; the sound set on the notification plays, as before.
To route a bundled sound and pick the channel yourself, send channel_id: "mobiweb_sound_<name>".
App in the background
When the app is in the background, a push with an FCM notification block is shown by the system, not by the app. The paired fcm-proxy change (Dwigoric/fcm-proxy#9) therefore sends pushes with a custom sound as data-only Android messages, so the app always renders them and routes them to the sound's channel. As with other data-only pushes, a force-stopped app only receives them after it is launched again.
How It Works (iOS)
iOS has no channels: the sound set on the notification plays, from a file in the app bundle.
Build
The App target has a Copy Notification Sounds Run Script phase (after Copy Bundle Resources) that runs ios/App/scripts/copy-notification-sounds.sh:
validates
sounds/with the rules above, and checks each file withafinfo;copies every
sounds/*.wavinto the root of the app bundle, next to sounds added to the App target directly;removes sounds it copied in an earlier build that are no longer in
sounds/.
Adding or removing a sound needs no Xcode project change. The phase writes an Xcode dependency file listing sounds/ and every file in it, so it only re-runs when a sound is added, removed or edited. An empty sounds/ (only README.md) builds as before. Tests: yarn test:ios-sounds.
Name Resolution
Pushes (Notification Service Extension), critical alerts, local notifications and foreground presentation resolve sound the same way (ios/App/Shared/NotificationSoundResolver.swift):
| Plays |
|---|---|
| System default ( |
| Nothing |
| That file, exactly as given (default sound if the file doesn't exist) |
| The first of |
| System default |
default and none match in any case. The extension looks sounds up in the containing app bundle, not in its own .appex. The proxy sends aps.sound exactly as the sender gave it; the extension replaces it with the resolved sound. If the extension doesn't run (it has about 30 seconds), iOS plays aps.sound as given, so a name without an extension falls back to the default sound in that case.