0

I started building a Material 3 Expressive music player app built mainly on Java (~85%).

To prevent confusion, I'll provide a direct link to the entire thing just in case: Main Player Service | CustomNotificationProvider

My main setup parts:

@Override 
public void onCreate() { 
 super.onCreate(); 
 fallbackUri = Uri.parse("android.resource://" + this.getPackageName() + "/" + resId);
 handlerThread.start();
 Looper backgroundLooper = handlerThread.getLooper();
 ExoPlayerHandler = new Handler(backgroundLooper);
 DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(this).setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON);
 player = new ExoPlayer.Builder(this, renderersFactory).setLooper(backgroundLooper).build();
 androidx.media3.common.AudioAttributes attrs = new androidx.media3.common.AudioAttributes.Builder()
 .setUsage(C.USAGE_MEDIA)
 .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
 .build();
 ExoPlayerHandler.post(() -> {
 player.setAudioAttributes(attrs, true);
 });
 handler = new Handler(Looper.getMainLooper()); 
 if (!isBuilt) {
 setupMediaSession();
 isBuilt = true;
 }
 isNotifDead = false;
 createNotificationChannel(); 
 audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
 setupAudioFocusRequest();
 setMediaNotificationProvider(new CustomNotificationProvider(this));
 if (Build.VERSION.SDK_INT >= 33) { 
 startForeground(NOTIFICATION_ID, buildNotification("XMusic", "No song is playing", "") , ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK); 
 } else { 
 startForeground(NOTIFICATION_ID, buildNotification("XMusic", "No song is playing", "")); 
 } 
}
private Notification buildNotification(String title, String artist, String cover) {
 if (mediaSession == null) {
 setupMediaSession();
 }
 MediaStyleNotificationHelper.MediaStyle mediaStyle = new MediaStyleNotificationHelper.MediaStyle(mediaSession).setShowCancelButton(true);
 Intent resumeIntent = c.getPackageManager().getLaunchIntentForPackage(c.getPackageName());
 PendingIntent contentIntent = PendingIntent.getActivity(c, 0, resumeIntent, PendingIntent.FLAG_IMMUTABLE);
 return new NotificationCompat.Builder(this, CHANNEL_ID)
 .setSmallIcon(R.mipmap.ic_launcher_foreground)
 .setContentTitle(title)
 .setContentText(artist)
 .setLargeIcon(current)
 .setStyle(mediaStyle)
 .setOngoing(true)
 .setContentIntent(contentIntent)
 .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
 .build();
}
private void setupMediaSession() {
 if (mediaSession != null) {
 return;
 }
 mediaSession = new androidx.media3.session.MediaSession.Builder(this, player).setId("XMusicMediaSessionPrivate").setCallback(new CustomCallback()).build();
}
public class CustomCallback implements MediaSession.Callback {
 
 final SessionCommand TEST = new SessionCommand("TEST_ACTION", Bundle.EMPTY);
 final CommandButton TEST_BUTTON = new CommandButton.Builder(R.drawable.ic_shuffle).setDisplayName("shuffle").setSessionCommand(TEST).setSlots(CommandButton.SLOT_FORWARD_SECONDARY).build();
 List<CommandButton> list = ImmutableList.of(TEST_BUTTON);
 Player.Commands pcs = new Player.Commands.Builder().addAllCommands().add(Player.COMMAND_SET_SHUFFLE_MODE).build();
 private SessionCommands sc = SessionCommands.EMPTY.buildUpon().add(TEST).build();
 @Override
 public ConnectionResult onConnect(MediaSession mediaSession, ControllerInfo controllerInfo) {
 //return new ConnectionResult.AcceptedResultBuilder(mediaSession).setCustomLayout(list).setAvailableSessionCommands(sc).setMediaButtonPreferences(ImmutableList.of(TEST_BUTTON)).build();
 return ConnectionResult.accept(sc, pcs);
 }
 
 @Override
 public void onPostConnect(MediaSession mediaSession, ControllerInfo controllerInfo) {
 mediaSession.setAvailableCommands(controllerInfo, sc, pcs);
 mediaSession.setCustomLayout(controllerInfo, list);
 }
 
 @Override
 public ListenableFuture<SessionResult> onCustomCommand(MediaSession mediaSession, ControllerInfo controllerInfo, SessionCommand sessionCommand, Bundle bundle) {
 return Futures.immediateFuture(new SessionResult(-6));
 }
}
public class CustomNotificationProvider extends DefaultMediaNotificationProvider {
 private Context c;
 public CustomNotificationProvider(Context context) {
 super(context);
 c = context;
 }
 
 @Override
 public int[] addNotificationActions(MediaSession mediaSession, ImmutableList<CommandButton> mediaButtons, NotificationCompat.Builder builder, MediaNotification.ActionFactory actionFactory) {
 CommandButton cb = new CommandButton.Builder(R.drawable.ic_shuffle).setDisplayName("negro").setSessionCommand(new SessionCommand("action_nigga", Bundle.EMPTY)).setSlots(CommandButton.SLOT_BACK_SECONDARY).build();
 ImmutableList<CommandButton> l = ImmutableList.of(cb);
 NotificationCompat.Action action = actionFactory.createCustomAction(mediaSession, IconCompat.createWithResource(c, R.drawable.ic_shuffle), "test", "test", Bundle.EMPTY);
 builder.addAction(action);
 int[] i = super.addNotificationActions(mediaSession, l, builder, actionFactory);
 return i;
 } 
 
 @Override
 public ImmutableList<CommandButton> getMediaButtons(MediaSession mediaSession, Player.Commands commands, ImmutableList<CommandButton> mediaButtonPreferences, boolean z) {
 CommandButton cb = new CommandButton.Builder(R.drawable.ic_shuffle).setDisplayName("negro").setSessionCommand(new SessionCommand("action_nigga", Bundle.EMPTY)).setSlots(CommandButton.SLOT_BACK_SECONDARY).build();
 ImmutableList<CommandButton> l = ImmutableList.of(cb);
 return l;
 }
 private PendingIntent getSessionActivityPendingIntent(MediaSession session) {
 return session.getSessionActivity();
 }
}

I've been trying to add custom notification actions to my media notification, but no matter what I tried, only default notification actions (seek buttons, seek bar and play/pause toggle) keep showing like this:

tyg
21.3k6 gold badges47 silver badges60 bronze badges
asked Oct 27, 2025 at 7:37
0

0

Know someone who can answer? Share a link to this question via email, Twitter, or Facebook.

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.