How to Customize Notification Sounds on Android
Hello everyone,
You might be familiar with handling notifications in Flutter using third-party plugins like flutter_local_notifications
and awesome_notifications
. However, there are times when you might prefer to manage notifications in a more traditional manner, such as with firebase_messaging
.
On the iOS side, it’s not too complicated. You can simply import sound files (e.g., .wav
, .aiff
, .caf
) into the root of your iOS folder. After that, you’ll need to add the sound name in your notification payload, but without the file extension. It would look something like this:
{
"message": {
"token": "your-registration-token",
"apns": {
"payload": {
"aps": {
"alert": {
"title": "Notification Title",
"body": "Notification Body"
},
"sound": "your-custom-sound"
}
}
}
}
}
And with that, you’re all set for iOS. On the other hand, Android requires a few more steps — or at least, that’s been my experience. For some reason, the example payload below doesn’t seem to work as expected on Android.
{
"message": {
"token": "your-registration-token",
"android": {
"notification": {
"title": "Notification Title",
"body": "Notification Body",
"sound": "your-custom-sound"
}
}
}
}
Android doesn’t always play your custom notification sound, even if you follow the same steps that work on iOS.
Manual set-up
Usually, iOS is the problematic sibling among mobile platforms, but this time, Android decided to steal the show. We can’t play custom sounds because, guess what? We don’t have a custom notification channel. Voilà!
By default, there’s only the default notification channel, and your payload doesn’t specify which channel to use for notifications. So, the question arises: How do we create these elusive notification channels?
Well, here’s the kicker — Firebase Messaging plugin doesn’t support creating channels. At least, I couldn’t find any magical button for it. So, yes, you’ll have to get your hands dirty and dive into native Android code. It’s unavoidable.
First things first, we need a sound file, say, in WAV format. You’ll need to place it in the source directory. Assuming you’re not setting custom sounds for different flavors, go ahead and drop your sound files under android > app > src > main > res > raw
. And if you don’t see a raw
directory, create one.
We’ll also need a few things:
- channel_id: This is your notification channel, and it should be unique because it’ll be used in the notification payload.
- title: This will be visible in the notification settings of your phone.
- description: This is basically the same as the title.
Let’s keep these values under resources. To do that, navigate to android > app > src > main > res > values
. If there’s no values
folder, you’ll need to create one. Then, create a strings.xml
file, and add your string resources like this:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="message_channel_name">Message Channel</string>
<string name="message_channel_description">This is used to receive chat message notifications</string>
<string name="message_channel_id">message_channel</string>
</resources>
We’ll use the name
attributes to access these strings later.
Now, it’s time to bring everything together. Head over to android > app > src > main > kotlin > path/to/package > MainActivity.kt
and add the following code:
package com.nahitfidanci.notification_example
import android.app.NotificationChannel
import android.app.NotificationManager
import android.media.AudioAttributes
import android.net.Uri
import android.os.Build
import android.os.Bundle
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initChatChannel()
}
private fun initChatChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channelId = getString(R.string.message_channel_id)
val name = getString(R.string.message_channel_name)
val descriptionText = getString(R.string.message_channel_description)
val importance = NotificationManager.IMPORTANCE_DEFAULT
val mChannel = NotificationChannel(channelId, name, importance)
mChannel.description = descriptionText
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(mChannel)
}
}
}
So, we access our channel information via R.string
. If you open the Android project in Android Studio, Intellisense should suggest the available string resources. If it doesn’t, no worries—just use the exact names you defined in the strings.xml
file. The getString
function helps us convert these resources to actual strings.
Now, we’ve created our notification channel. And here’s the kicker: after you send your notifications with the unique channel_id
you’ve set here, you might still hear the default sound. How cool is that!
At this point, I assume you’re a bit confused. Trust me, I was too. It turns out that we need to set the sound during the channel creation process. Now, you might be asking:
“Nahit, if we’re sending the sound name through the payload, why on earth do we need to set the sound here as well?”
Well, my friend, I asked myself the exact same question and couldn’t find a satisfying answer. If you know what I’m missing, please leave a comment and fill us in!
Let’s make a small addition to the previous code.
package com.nahitfidanci.notification_example
import android.app.NotificationChannel
import android.app.NotificationManager
import android.media.AudioAttributes
import android.net.Uri
import android.os.Build
import android.os.Bundle
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initChatChannel()
}
private fun initChatChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channelId = getString(R.string.message_channel_id)
val name = getString(R.string.message_channel_name)
val descriptionText = getString(R.string.message_channel_description)
val importance = NotificationManager.IMPORTANCE_DEFAULT
val mChannel = NotificationChannel(channelId, name, importance)
mChannel.description = descriptionText
// Set the custom sound URI
val soundName = "raw/message_tone"
val soundUri =
Uri.parse("android.resource://${context.packageName}/" + soundName)
val audioAttributes = AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
.build()
mChannel.setSound(soundUri, audioAttributes)
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(mChannel)
}
}
}
We should be able to access the raw sound file similarly to how we retrieve string resources, but for some reason, I couldn’t get it to work. This is a workaround — I manually build the path myself. If you encounter a similar issue, you can create the sound path just as I did.
Once you’ve set up your notification channel, if you send a notification with the corresponding channel_id
, you’ll hear the custom sound.
Important Note
Once you set a notification channel, you cannot change its settings. This includes the title, description, channel_id
, and, of course, the sound. If a channel is created without a custom sound, you won’t be able to add one later. The only way for the user to see these changes is to permanently delete your app and reinstall it. Keep this in mind.
Conclusion
And there you have it — Android’s quirky notification channels in all their glory. Who knew setting a custom sound could turn into such a saga?
But hey, you’ve got this now. If this guide saved you some headaches (or at least gave you a chuckle), go ahead and hit that clap button. You’ve earned it!
Thank you for reading.