Android notification sound inconsistent during Do Not Disturb when two notifications arrive close together
I have an Android app that receives FCM messages and posts notifications on two channels:
A generic/default channel (IMPORTANCE_DEFAULT)
A critical channel (IMPORTANCE_HIGH, setBypassDnd(true))
Critical notifications should alert the user even when Do Not Disturb (DND) is enabled (when the user has allowed it)
When two notifications (generic + critical) arrive very close together:
Sometimes only one notification sound is played
Sometimes the critical one does not sound during DND
Behavior varies by timing and device
Does Android guarantee separate sounds when two notifications arrive at nearly the same time?
Is there a supported way to ensure the critical notification reliably alerts during DND without modifying global audio state?
Is this expected behavior of the Android notification system?
Below are some code snippets:
@HiltAndroidApp
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
initNotification()
}
private fun initNotification() {
FirebaseApp.initializeApp(this)
val audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.build()
val defaultSoundUri =
("android.resource://" + this.packageName + "/" + R.raw.default_notification).toUri()
val channel = NotificationChannel(
getString(R.string.new_default_notification_channel_id),
getString(R.string.default_notification_channel_name),
NotificationManager.IMPORTANCE_DEFAULT
).apply {
setSound(defaultSoundUri, audioAttributes)
}
val criticalAudioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ALARM)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.build()
val criticalSoundUri =
("android.resource://" + this.packageName + "/" + R.raw.criticalalert).toUri()
val criticalAlertsChannel = NotificationChannel(
/* id = */ getString(R.string.critical_notification_channel_id),
/* name = */ getString(R.string.critical_notification_channel_name),
/* importance = */ NotificationManager.IMPORTANCE_HIGH
).apply {
setSound(criticalSoundUri, criticalAudioAttributes)
enableVibration(true)
vibrationPattern = longArrayOf(0, 250, 250, 250)
description = getString(R.string.critical_notification_channel_desc)
setBypassDnd(true)
}
val notificationManager: NotificationManager = getSystemService(
NOTIFICATION_SERVICE
) as NotificationManager
notificationManager.createNotificationChannel(criticalAlertsChannel)
notificationManager.createNotificationChannel(channel)
}
}
@AndroidEntryPoint
class MessagingService : FirebaseMessagingService() {
override fun onMessageReceived(message: RemoteMessage) {
val messageData = MessageData(message.data)
val overrideDoNotDisturb = (messageData.isCritical == "1" && notificationManager.isNotificationPolicyAccessGranted)
val notificationId = Random.nextInt(from = 1, until = 1000)
val pendingIntent = messageData.alert?.let { rawAlert ->
val alert = json.decodeFromString<Alert>(rawAlert)
when (messageData.alertType) {
ALERT_TYPE_INCIDENT -> getIncidentIntent(notificationId, alert.id)
else -> null
}
} ?: safeLet(messageData.title, messageData.body) { title, body ->
getGenericNotificationIntent(notificationId, title, body, messageData.url)
}
val channelId = if (overrideDoNotDisturb) {
getString(R.string.critical_notification_channel_id)
} else {
getString(R.string.new_default_notification_channel_id)
}
val builder = NotificationCompat.Builder(this, channelId)
.setSmallIcon(R.drawable.ic_alert_sa_notification)
.setColor(ContextCompat.getColor(this, R.color.primary))
.setContentTitle(messageData.title)
.setContentText(messageData.body)
.setStyle(NotificationCompat.BigTextStyle().bigText(messageData.body))
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
if (overrideDoNotDisturb) {
builder.setPriority(NotificationCompat.PRIORITY_MAX)
builder.setCategory(NotificationCompat.CATEGORY_ALARM)
val audioManager = getSystemService(AUDIO_SERVICE) as AudioManager
audioManager.ringerMode = AudioManager.RINGER_MODE_NORMAL
audioManager.setStreamVolume(
AudioManager.STREAM_NOTIFICATION,
audioManager.getStreamMaxVolume(AudioManager.STREAM_NOTIFICATION),
AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE
)
notificationManager.notify(notificationId, builder.build())
} else {
// Set a default priority for regular notifications
builder.setPriority(NotificationCompat.PRIORITY_DEFAULT)
// Delay regular notification to allow time for the critical alerts to play first
Handler(Looper.getMainLooper()).postDelayed({
notificationManager.notify(notificationId, builder.build())
}, REGULAR_NOTIFICATION_DELAY)
}
}
}