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
lang-java