Skip to content

Android expanded notification controls do not emit events because MEDIA_BUTTON intents are not handled #1273

Description

@k-ljung

Summary

On Android with react-native-audio-api@0.13.3, playback controls behave differently depending on which notification UI is used:

  • Compact notification widget controls work.
  • Scrubbing works.
  • Expanded notification / lock-screen play, pause, next, and previous controls do not emit the expected playbackNotification* events.
  • In a high-frequency update path, we also observed a java.util.ConcurrentModificationException from NotificationCompatBuilder while the notification was being updated.

I searched existing issues/PRs for MEDIA_BUTTON, PlaybackNotificationReceiver, expanded notification controls, and ConcurrentModificationException NotificationCompatBuilder, but did not find an existing report for this exact Android path.

Important MRE note

I know the issue template strongly encourages a minimal reproducible example. This report is currently based on a real app reproduction, not a public MRE yet. I am filing now because the code-level cause appears small and concrete. If needed, I can follow up with a minimal public repro project that exercises only PlaybackNotificationManager.show, enableControl, and event listeners.

Environment

  • react-native-audio-api: 0.13.3
  • React Native: 0.81.5
  • Expo SDK: 54
  • Android: reproduced on Android device/build from an Expo React Native app
  • New Architecture enabled
  • react-native-worklets: 0.6.1

Reproduction observed in app

  1. Configure and show playback notification metadata.
  2. Enable controls:
    • play
    • pause
    • nextTrack
    • previousTrack
    • skipForward
    • skipBackward
    • seekTo
  3. Start playback.
  4. Swipe down to show the compact notification widget.
  5. Press controls from the compact widget.
  6. Expand the notification or use lock-screen media controls.
  7. Press play/pause/next/previous.

Expected behavior

Expanded notification and lock-screen play/pause/next/previous should emit the same events as the compact notification widget:

  • playbackNotificationPlay
  • playbackNotificationPause
  • playbackNotificationNextTrack
  • playbackNotificationPreviousTrack

Actual behavior

Compact widget controls work, but expanded notification / lock-screen play/pause/next/previous controls do not emit the expected events. Scrubbing still works.

Code-level analysis

The compact controls appear to work through MediaSessionCompat.Callback in PlaybackNotification.kt:

override fun onPlay() {
  audioAPIModule.get()?.invokeHandlerWithEventNameAndEventBody(AudioEvent.PLAYBACK_NOTIFICATION_PLAY.ordinal, mapOf())
}

override fun onPause() {
  audioAPIModule.get()?.invokeHandlerWithEventNameAndEventBody(AudioEvent.PLAYBACK_NOTIFICATION_PAUSE.ordinal, mapOf())
}

override fun onSkipToNext() {
  audioAPIModule.get()?.invokeHandlerWithEventNameAndEventBody(AudioEvent.PLAYBACK_NOTIFICATION_NEXT_TRACK.ordinal, mapOf())
}

override fun onSkipToPrevious() {
  audioAPIModule.get()?.invokeHandlerWithEventNameAndEventBody(AudioEvent.PLAYBACK_NOTIFICATION_PREVIOUS_TRACK.ordinal, mapOf())
}

But expanded notification actions are created as pending intents with action PlaybackNotification.MEDIA_BUTTON:

val intent = Intent(MEDIA_BUTTON)
intent.setPackage(context.packageName)
intent.putExtra(Intent.EXTRA_KEY_EVENT, KeyEvent(KeyEvent.ACTION_DOWN, keyCode))

MediaSessionManager.kt registers that action on PlaybackNotificationReceiver:

playbackFilter.addAction(PlaybackNotification.MEDIA_BUTTON)

However, PlaybackNotificationReceiver.kt handles only:

  • ACTION_NOTIFICATION_DISMISSED
  • ACTION_SKIP_FORWARD
  • ACTION_SKIP_BACKWARD

It does not handle PlaybackNotification.MEDIA_BUTTON or decode Intent.EXTRA_KEY_EVENT.

That seems to explain the split behavior: compact MediaSession callbacks work, while expanded PendingIntent-backed media button actions are dropped.

Local patch that fixed the app behavior

This local patch maps MEDIA_BUTTON key events to the same AudioEvent values as the MediaSession callback path:

diff --git a/android/src/main/java/com/swmansion/audioapi/system/notification/PlaybackNotificationReceiver.kt b/android/src/main/java/com/swmansion/audioapi/system/notification/PlaybackNotificationReceiver.kt
--- a/android/src/main/java/com/swmansion/audioapi/system/notification/PlaybackNotificationReceiver.kt
+++ b/android/src/main/java/com/swmansion/audioapi/system/notification/PlaybackNotificationReceiver.kt
@@ -3,6 +3,7 @@ package com.swmansion.audioapi.system.notification
 import android.content.BroadcastReceiver
 import android.content.Context
 import android.content.Intent
+import android.view.KeyEvent
 import com.swmansion.audioapi.AudioAPIModule
 import com.swmansion.audioapi.system.AudioEvent
 
@@ -40,6 +41,43 @@ class PlaybackNotificationReceiver : BroadcastReceiver() {
         val body = HashMap<String, Any>().apply { put("value", 15) }
         audioAPIModule?.invokeHandlerWithEventNameAndEventBody(AudioEvent.PLAYBACK_NOTIFICATION_SKIP_BACKWARD.ordinal, body)
       }
+
+      PlaybackNotification.MEDIA_BUTTON -> {
+        val keyEvent = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
+          intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT, KeyEvent::class.java)
+        } else {
+          @Suppress("DEPRECATION")
+          intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT)
+        }
+
+        if (keyEvent?.action != KeyEvent.ACTION_DOWN) return
+
+        when (keyEvent.keyCode) {
+          KeyEvent.KEYCODE_MEDIA_PLAY -> {
+            audioAPIModule?.invokeHandlerWithEventNameAndEventBody(AudioEvent.PLAYBACK_NOTIFICATION_PLAY.ordinal, mapOf())
+          }
+          KeyEvent.KEYCODE_MEDIA_PAUSE -> {
+            audioAPIModule?.invokeHandlerWithEventNameAndEventBody(AudioEvent.PLAYBACK_NOTIFICATION_PAUSE.ordinal, mapOf())
+          }
+          KeyEvent.KEYCODE_MEDIA_STOP -> {
+            audioAPIModule?.invokeHandlerWithEventNameAndEventBody(AudioEvent.PLAYBACK_NOTIFICATION_STOP.ordinal, mapOf())
+          }
+          KeyEvent.KEYCODE_MEDIA_NEXT -> {
+            audioAPIModule?.invokeHandlerWithEventNameAndEventBody(AudioEvent.PLAYBACK_NOTIFICATION_NEXT_TRACK.ordinal, mapOf())
+          }
+          KeyEvent.KEYCODE_MEDIA_PREVIOUS -> {
+            audioAPIModule?.invokeHandlerWithEventNameAndEventBody(AudioEvent.PLAYBACK_NOTIFICATION_PREVIOUS_TRACK.ordinal, mapOf())
+          }
+          KeyEvent.KEYCODE_MEDIA_FAST_FORWARD -> {
+            val body = HashMap<String, Any>().apply { put("value", 15) }
+            audioAPIModule?.invokeHandlerWithEventNameAndEventBody(AudioEvent.PLAYBACK_NOTIFICATION_SKIP_FORWARD.ordinal, body)
+          }
+          KeyEvent.KEYCODE_MEDIA_REWIND -> {
+            val body = HashMap<String, Any>().apply { put("value", 15) }
+            audioAPIModule?.invokeHandlerWithEventNameAndEventBody(AudioEvent.PLAYBACK_NOTIFICATION_SKIP_BACKWARD.ordinal, body)
+          }
+        }
+      }
     }
   }
 }

Related crash observed

Before serializing notification updates in app code, we also saw this Android crash while notification controls/metadata were being updated:

java.util.ConcurrentModificationException
  java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1112)
  java.util.ArrayList$Itr.next(ArrayList.java:1065)
  androidx.core.app.NotificationCompatBuilder.<init>(NotificationCompatBuilder.java:127)
  androidx.core.app.NotificationCompat$Builder.build(NotificationCompat.java:2528)
  com.swmansion.audioapi.system.notification.PlaybackNotification.buildNotification(PlaybackNotification.kt:515)
  com.swmansion.audioapi.system.notification.PlaybackNotification.updateInternal$lambda$1$lambda$0(PlaybackNotification.kt:249)

We mitigated that separately in app code by serializing show, hide, and enableControl calls, so the main upstream issue here is the missing MEDIA_BUTTON handling path.

Cache / rebuild status

This was tested after rebuilding the native Android app. The Expo/Metro cache was also reset during upgrade testing. The local patch requires a native rebuild to take effect.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    androidNative Android implementation, C++/Java/Kotlin bindings, or Android-specific issuesbugSomething isn't working as expected or produces unexpected errors

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions