software-mansion / software-mansion/react-native-audio-api

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

Open Beginner friendly
#1,273 5 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

android bug
Dominant language
C++
Stars
839
Forks
92
Avg merge
4d 15h
Merged PRs (30d)
27

Description

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with android/src/main/java/com/swmansion/audioapi/system/notification/PlaybackNotificationReceiver.kt and compare its handled actions with MEDIA_BUTTON registration in MediaSessionManager.kt. Trace the Intent.EXTRA_KEY_EVENT path and verify that expanded and lock-screen controls emit the expected playback notification events after a native Android rebuild.

Written by the indexing model from the issue text.

Assessment

Tech stack
android, kotlin, react-native
Domain
mobile-dev
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
82/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.