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

  1. Does Android guarantee separate sounds when two notifications arrive at nearly the same time?

  2. Is there a supported way to ensure the critical notification reliably alerts during DND without modifying global audio state?

  3. 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)
        }
    }
}
1
Feb 2 at 12:08 AM
User AvatarJean Tadebois
#android#notifications

No answer found for this question yet.