Mediensteuerung

Die Mediensteuerung in Android befindet sich in der Nähe der Schnelleinstellungen. Sitzungen aus mehreren Apps werden in einem wischbaren Karussell angeordnet. Im Karussell werden die Sitzungen in dieser Reihenfolge aufgeführt:

  • Streams, die lokal auf dem Smartphone abgespielt werden
  • Remote-Streams, z. B. solche, die auf externen Geräten oder in Cast-Sitzungen erkannt werden
  • Vorherige fortsetzbare Sitzungen in der Reihenfolge, in der sie zuletzt abgespielt wurden

Ab Android 13 (API-Level 33) werden Aktionsschaltflächen in der Mediensteuerung vom Player-Status abgeleitet, damit Nutzer auf eine Vielzahl von Steuerelementen für die Medienwiedergabe in Apps zugreifen können.

So kannst du auf allen Geräten eine einheitliche Mediensteuerung und eine optimierte Mediensteuerung anbieten.

Abbildung 1 zeigt ein Beispiel dafür, wie das auf einem Smartphone und einem Tablet aussieht.

Mediensteuerung auf Smartphones und Tablets, wobei anhand eines Beispieltracks gezeigt wird, wie die Schaltflächen aussehen können
Abbildung 1: Mediensteuerung auf Smartphones und Tablets

Das System zeigt bis zu fünf Aktionsbuttons basierend auf dem Player-Status an, wie in der folgenden Tabelle beschrieben. Im Kompaktmodus werden nur die ersten drei Aktionsslots angezeigt. Dies entspricht der Darstellung der Mediensteuerung auf anderen Android-Plattformen wie Auto, Assistant und Wear OS.

Slot Kriterien Aktion
1 playWhenReady ist „false“ oder der aktuelle Wiedergabestatus ist STATE_ENDED. Play
playWhenReady ist „true“ und der aktuelle Wiedergabestatus ist STATE_BUFFERING. Rotierendes Ladesymbol
playWhenReady ist „true“ und der aktuelle Wiedergabestatus ist STATE_READY. Pausieren
2 Die Einstellungen für Medientasten enthalten eine benutzerdefinierte Taste für CommandButton.SLOT_BACK. Benutzerdefiniert
Der Player-Befehl COMMAND_SEEK_TO_PREVIOUS oder COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM ist verfügbar. Zurück
Weder eine benutzerdefinierte Taste noch einer der aufgeführten Befehle ist verfügbar. Leer
3 Die Einstellungen für Medientasten enthalten eine benutzerdefinierte Taste für CommandButton.SLOT_FORWARD. Benutzerdefiniert
Der Player-Befehl COMMAND_SEEK_TO_NEXT oder COMMAND_SEEK_TO_NEXT_MEDIA_ITEM ist verfügbar. Weiter
Weder eine benutzerdefinierte Taste noch einer der aufgeführten Befehle ist verfügbar. Leer
4 Die Einstellungen für Medientasten enthalten eine benutzerdefinierte Taste für CommandButton.SLOT_OVERFLOW, die noch nicht platziert wurde. Benutzerdefiniert
5 Die Einstellungen für Medientasten enthalten eine benutzerdefinierte Taste für CommandButton.SLOT_OVERFLOW, die noch nicht platziert wurde. Benutzerdefiniert

Benutzerdefinierte Overflow-Buttons werden in der Reihenfolge platziert, in der sie den Einstellungen für Medien-Buttons hinzugefügt wurden.

Befehlsschaltflächen anpassen

Wenn Sie die Systemmediensteuerung mit Jetpack Media3 anpassen möchten, können Sie die Einstellungen für Medientasten der Sitzung und die verfügbaren Befehle der Controller entsprechend festlegen:

  1. Erstelle eine MediaSession und definiere die Einstellungen für Medientasten für benutzerdefinierte Befehlsschaltflächen.

  2. In MediaSession.Callback.onConnect() autorisieren Sie Controller, indem Sie ihre verfügbaren Befehle, einschließlich benutzerdefinierter Befehle, in der ConnectionResult definieren.

  3. Reagieren Sie in MediaSession.Callback.onCustomCommand() auf den vom Nutzer ausgewählten benutzerdefinierten Befehl.

Kotlin

class PlaybackService : MediaSessionService() {
  private val customCommandFavorites = SessionCommand(ACTION_FAVORITES, Bundle.EMPTY)
  private var mediaSession: MediaSession? = null

  override fun onCreate() {
    super.onCreate()
    val favoriteButton =
      CommandButton.Builder(CommandButton.ICON_HEART_UNFILLED)
        .setDisplayName("Save to favorites")
        .setSessionCommand(customCommandFavorites)
        .build()
    val player = ExoPlayer.Builder(this).build()
    // Build the session with a custom layout.
    mediaSession =
      MediaSession.Builder(this, player)
        .setCallback(MyCallback())
        .setMediaButtonPreferences(ImmutableList.of(favoriteButton))
        .build()
  }

  private inner class MyCallback : MediaSession.Callback {
    override fun onConnect(
      session: MediaSession,
      controller: MediaSession.ControllerInfo
    ): ConnectionResult {
    // Set available player and session commands.
    return AcceptedResultBuilder(session)
      .setAvailableSessionCommands(
        ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
          .add(customCommandFavorites)
          .build()
      )
      .build()
    }

    override fun onCustomCommand(
      session: MediaSession,
      controller: MediaSession.ControllerInfo,
      customCommand: SessionCommand,
      args: Bundle
    ): ListenableFuture {
      if (customCommand.customAction == ACTION_FAVORITES) {
        // Do custom logic here
        saveToFavorites(session.player.currentMediaItem)
        return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
      }
      return super.onCustomCommand(session, controller, customCommand, args)
    }
  }
}

Java

public class PlaybackService extends MediaSessionService {
  private static final SessionCommand CUSTOM_COMMAND_FAVORITES =
      new SessionCommand("ACTION_FAVORITES", Bundle.EMPTY);
  @Nullable private MediaSession mediaSession;

  public void onCreate() {
    super.onCreate();
    CommandButton favoriteButton =
        new CommandButton.Builder(CommandButton.ICON_HEART_UNFILLED)
            .setDisplayName("Save to favorites")
            .setSessionCommand(CUSTOM_COMMAND_FAVORITES)
            .build();
    Player player = new ExoPlayer.Builder(this).build();
    // Build the session with a custom layout.
    mediaSession =
        new MediaSession.Builder(this, player)
            .setCallback(new MyCallback())
            .setMediaButtonPreferences(ImmutableList.of(favoriteButton))
            .build();
  }

  private static class MyCallback implements MediaSession.Callback {
    @Override
    public ConnectionResult onConnect(
        MediaSession session, MediaSession.ControllerInfo controller) {
      // Set available player and session commands.
      return new AcceptedResultBuilder(session)
          .setAvailableSessionCommands(
              ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
                .add(CUSTOM_COMMAND_FAVORITES)
                .build())
          .build();
    }

    public ListenableFuture onCustomCommand(
        MediaSession session,
        MediaSession.ControllerInfo controller,
        SessionCommand customCommand,
        Bundle args) {
      if (customCommand.customAction.equals(CUSTOM_COMMAND_FAVORITES.customAction)) {
        // Do custom logic here
        saveToFavorites(session.getPlayer().getCurrentMediaItem());
        return Futures.immediateFuture(new SessionResult(SessionResult.RESULT_SUCCESS));
      }
      return MediaSession.Callback.super.onCustomCommand(
          session, controller, customCommand, args);
    }
  }
}

Weitere Informationen zum Konfigurieren der MediaSession, damit Clients wie das System eine Verbindung zu deiner Medien-App herstellen können, findest du unter Anderen Clients die Steuerung erlauben.

Wenn du mit Jetpack Media3 eine MediaSession implementierst, wird deine PlaybackState automatisch mit dem Mediaplayer synchronisiert. Wenn Sie einen MediaSessionService implementieren, veröffentlicht die Bibliothek automatisch eine MediaStyle-Benachrichtigung für Sie und hält sie auf dem neuesten Stand.

Auf Aktionsbuttons reagieren

Wenn ein Nutzer auf einen Aktionsbutton in der Mediensteuerung des Systems tippt, sendet die MediaController des Systems einen Wiedergabebefehl an deine MediaSession. Die MediaSession leitet diese Befehle dann an den Player weiter. Befehle, die in der Player-Schnittstelle von Media3 definiert sind, werden automatisch von der Mediensitzung verarbeitet.

Informationen zum Antworten auf einen benutzerdefinierten Befehl finden Sie unter Benutzerdefinierte Befehle hinzufügen.

Fortsetzen der Medienwiedergabe unterstützen

Mit der Funktion „Medienfortsetzung“ können Nutzer vorherige Sitzungen über das Karussell neu starten, ohne die App öffnen zu müssen. Wenn die Wiedergabe beginnt, interagiert der Nutzer wie gewohnt mit der Mediensteuerung.

Die Funktion zur Fortsetzung der Wiedergabe kann in den Einstellungen unter Ton > Medien aktiviert und deaktiviert werden. Der Nutzer kann auch auf die Einstellungen zugreifen, indem er auf das Zahnradsymbol tippt, das nach dem Wischen im maximierten Karussell angezeigt wird.

Media3 bietet APIs, die die Unterstützung der Medienfortsetzung vereinfachen. Informationen zur Implementierung dieser Funktion findest du in der Dokumentation zur Wiederaufnahme der Wiedergabe mit Media3.

Legacy-Media-APIs verwenden

In diesem Abschnitt wird erläutert, wie du die Mediensteuerung des Systems mithilfe der alten MediaCompat APIs einbindest.

Das System ruft die folgenden Informationen aus der MediaMetadata von MediaSession ab und zeigt sie an, wenn sie verfügbar sind:

  • METADATA_KEY_ALBUM_ART_URI
  • METADATA_KEY_TITLE
  • METADATA_KEY_DISPLAY_TITLE
  • METADATA_KEY_ARTIST
  • METADATA_KEY_DURATION (Wenn die Dauer nicht festgelegt ist, wird auf der Steuerleiste kein Fortschritt angezeigt)

Damit du eine gültige und korrekte Benachrichtigung zur Mediensteuerung erhältst, setze den Wert der Metadaten METADATA_KEY_TITLE oder METADATA_KEY_DISPLAY_TITLE auf den Titel des aktuell wiedergegebenen Mediums.

Der Mediaplayer zeigt die verstrichene Zeit für die aktuell wiedergegebenen Medien sowie eine Steuerleiste an, die dem MediaSession PlaybackState zugeordnet ist.

Der Mediaplayer zeigt den Fortschritt für die aktuell wiedergegebenen Medien sowie eine Steuerleiste an, die dem MediaSession PlaybackState zugeordnet ist. Über die Steuerleiste können Nutzer die Position ändern. Außerdem wird die verstrichene Zeit für die Mediendatei angezeigt. Damit die Steuerleiste aktiviert werden kann, musst du PlaybackState.Builder#setActions implementieren und ACTION_SEEK_TO einfügen.

Slot Aktion Kriterien
1 Play Der aktuelle Status des PlaybackState ist einer der folgenden:
  • STATE_NONE
  • STATE_STOPPED
  • STATE_PAUSED
  • STATE_ERROR
Rotierendes Ladesymbol Der aktuelle Status des PlaybackState ist einer der folgenden:
  • STATE_CONNECTING
  • STATE_BUFFERING
Pausieren Der aktuelle Status des PlaybackState ist keiner der oben genannten.
2 Zurück PlaybackState Aktionen umfassen ACTION_SKIP_TO_PREVIOUS.
Benutzerdefiniert PlaybackState Aktionen enthalten nicht ACTION_SKIP_TO_PREVIOUS und PlaybackState benutzerdefinierte Aktionen enthalten eine benutzerdefinierte Aktion, die noch nicht platziert wurde.
Leer PlaybackState Extras enthalten einen booleschen Wert true für den Schlüssel SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV.
3 Weiter PlaybackState Aktionen umfassen ACTION_SKIP_TO_NEXT.
Benutzerdefiniert PlaybackState Aktionen enthalten nicht ACTION_SKIP_TO_NEXT und PlaybackState benutzerdefinierte Aktionen enthalten eine benutzerdefinierte Aktion, die noch nicht platziert wurde.
Leer PlaybackState Extras enthalten einen booleschen Wert true für den Schlüssel SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT.
4 Benutzerdefiniert PlaybackState benutzerdefinierte Aktion enthält eine benutzerdefinierte Aktion, die noch nicht platziert wurde.
5 Benutzerdefiniert PlaybackState benutzerdefinierte Aktion enthält eine benutzerdefinierte Aktion, die noch nicht platziert wurde.

Standardaktionen hinzufügen

In den folgenden Codebeispielen wird gezeigt, wie Sie PlaybackState Standard- und benutzerdefinierte Aktionen hinzufügen.

Für die Wiedergabe, Pause, den vorherigen und den nächsten Titel legen Sie diese Aktionen in der PlaybackState für die Mediensitzung fest.

Kotlin

val session = MediaSessionCompat(context, TAG)
val playbackStateBuilder = PlaybackStateCompat.Builder()
val style = NotificationCompat.MediaStyle()

// For this example, the media is currently paused:
val state = PlaybackStateCompat.STATE_PAUSED
val position = 0L
val playbackSpeed = 1f
playbackStateBuilder.setState(state, position, playbackSpeed)

// And the user can play, skip to next or previous, and seek
val stateActions = PlaybackStateCompat.ACTION_PLAY
    or PlaybackStateCompat.ACTION_PLAY_PAUSE
    or PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
    or PlaybackStateCompat.ACTION_SKIP_TO_NEXT
    or PlaybackStateCompat.ACTION_SEEK_TO // adding the seek action enables seeking with the seekbar
playbackStateBuilder.setActions(stateActions)

// ... do more setup here ...

session.setPlaybackState(playbackStateBuilder.build())
style.setMediaSession(session.sessionToken)
notificationBuilder.setStyle(style)

Java

MediaSessionCompat session = new MediaSessionCompat(context, TAG);
PlaybackStateCompat.Builder playbackStateBuilder = new PlaybackStateCompat.Builder();
NotificationCompat.MediaStyle style = new NotificationCompat.MediaStyle();

// For this example, the media is currently paused:
int state = PlaybackStateCompat.STATE_PAUSED;
long position = 0L;
float playbackSpeed = 1f;
playbackStateBuilder.setState(state, position, playbackSpeed);

// And the user can play, skip to next or previous, and seek
long stateActions = PlaybackStateCompat.ACTION_PLAY
    | PlaybackStateCompat.ACTION_PLAY_PAUSE
    | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
    | PlaybackStateCompat.ACTION_SKIP_TO_NEXT
    | PlaybackStateCompat.ACTION_SEEK_TO; // adding this enables the seekbar thumb
playbackStateBuilder.setActions(stateActions);

// ... do more setup here ...

session.setPlaybackState(playbackStateBuilder.build());
style.setMediaSession(session.getSessionToken());
notificationBuilder.setStyle(style);

Wenn du keine Buttons in den vorherigen oder nächsten Slots haben möchtest, füge nicht ACTION_SKIP_TO_PREVIOUS oder ACTION_SKIP_TO_NEXT hinzu, sondern füge der Sitzung Extras hinzu:

Kotlin

session.setExtras(Bundle().apply {
    putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV, true)
    putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT, true)
})

Java

Bundle extras = new Bundle();
extras.putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV, true);
extras.putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT, true);
session.setExtras(extras);

Benutzerdefinierte Aktionen hinzufügen

Für andere Aktionen, die in der Mediensteuerung angezeigt werden sollen, kannst du stattdessen eine PlaybackStateCompat.CustomAction erstellen und diese der PlaybackState hinzufügen. Diese Aktionen werden in der Reihenfolge angezeigt, in der sie hinzugefügt wurden.

Kotlin

val customAction = PlaybackStateCompat.CustomAction.Builder(
    "com.example.MY_CUSTOM_ACTION", // action ID
    "Custom Action", // title - used as content description for the button
    R.drawable.ic_custom_action
).build()

playbackStateBuilder.addCustomAction(customAction)

Java

PlaybackStateCompat.CustomAction customAction = new PlaybackStateCompat.CustomAction.Builder(
        "com.example.MY_CUSTOM_ACTION", // action ID
        "Custom Action", // title - used as content description for the button
        R.drawable.ic_custom_action
).build();

playbackStateBuilder.addCustomAction(customAction);

Auf PlaybackState-Aktionen reagieren

Wenn ein Nutzer auf eine Schaltfläche tippt, verwendet SystemUI MediaController.TransportControls, um einen Befehl an die MediaSession zurückzusenden. Sie müssen einen Callback registrieren, der auf diese Ereignisse reagieren kann.

Kotlin

val callback = object: MediaSession.Callback() {
    override fun onPlay() {
        // start playback
    }

    override fun onPause() {
        // pause playback
    }

    override fun onSkipToPrevious() {
        // skip to previous
    }

    override fun onSkipToNext() {
        // skip to next
    }

    override fun onSeekTo(pos: Long) {
        // jump to position in track
    }

    override fun onCustomAction(action: String, extras: Bundle?) {
        when (action) {
            CUSTOM_ACTION_1 -> doCustomAction1(extras)
            CUSTOM_ACTION_2 -> doCustomAction2(extras)
            else -> {
                Log.w(TAG, "Unknown custom action $action")
            }
        }
    }

}

session.setCallback(callback)

Java

MediaSession.Callback callback = new MediaSession.Callback() {
    @Override
    public void onPlay() {
        // start playback
    }

    @Override
    public void onPause() {
        // pause playback
    }

    @Override
    public void onSkipToPrevious() {
        // skip to previous
    }

    @Override
    public void onSkipToNext() {
        // skip to next
    }

    @Override
    public void onSeekTo(long pos) {
        // jump to position in track
    }

    @Override
    public void onCustomAction(String action, Bundle extras) {
        if (action.equals(CUSTOM_ACTION_1)) {
            doCustomAction1(extras);
        } else if (action.equals(CUSTOM_ACTION_2)) {
            doCustomAction2(extras);
        } else {
            Log.w(TAG, "Unknown custom action " + action);
        }
    }
};

Medienwiedergabe fortsetzen

Damit deine Player-App im Bereich für die Schnelleinstellungen angezeigt wird, musst du eine MediaStyle-Benachrichtigung mit einem gültigen MediaSession-Token erstellen.

Verwende NotificationBuilder.setContentTitle(), um den Titel für die MediaStyle-Benachrichtigung anzuzeigen.

Verwende NotificationBuilder.setSmallIcon(), um das Markensymbol für den Mediaplayer anzuzeigen.

Damit die Wiedergabe fortgesetzt werden kann, müssen Apps einen MediaBrowserService und einen MediaSession implementieren. In Ihrem MediaSession muss der Callback onPlay() implementiert sein.

MediaBrowserService-Implementierung

Nach dem Start des Geräts sucht das System nach den fünf zuletzt verwendeten Medien-Apps und stellt Steuerelemente bereit, mit denen die Wiedergabe in jeder App neu gestartet werden kann.

Das System versucht, dein MediaBrowserService über eine Verbindung von SystemUI zu kontaktieren. Ihre App muss solche Verbindungen zulassen, da sie sonst die Fortsetzung der Wiedergabe nicht unterstützen kann.

Verbindungen von SystemUI können anhand des Paketnamens com.android.systemui und der Signatur identifiziert und verifiziert werden. Die SystemUI ist mit der Plattformsignatur signiert. Ein Beispiel für die Prüfung anhand der Plattformsignatur findest du in der UAMP-App.

Damit die Wiedergabe fortgesetzt werden kann, muss dein MediaBrowserService die folgenden Verhaltensweisen implementieren:

  • onGetRoot() muss schnell einen Stamm zurückgeben, der nicht null ist. Andere komplexe Logik sollte in onLoadChildren() verarbeitet werden.

  • Wenn onLoadChildren() für die Stamm-Medien-ID aufgerufen wird, muss das Ergebnis ein untergeordnetes Element mit dem Flag FLAG_PLAYABLE enthalten.

  • MediaBrowserService sollte die zuletzt wiedergegebene Mediendatei zurückgeben, wenn eine EXTRA_RECENT-Abfrage empfangen wird. Der zurückgegebene Wert sollte eine tatsächliche Mediendatei und keine generische Funktion sein.

  • MediaBrowserService muss eine entsprechende MediaDescription mit einem nicht leeren Titel und Untertitel enthalten. Außerdem sollte ein Symbol-URI oder eine Symbol-Bitmap festgelegt werden.

Die folgenden Codebeispiele veranschaulichen, wie onGetRoot() implementiert wird.

Kotlin

override fun onGetRoot(
    clientPackageName: String,
    clientUid: Int,
    rootHints: Bundle?
): BrowserRoot? {
    ...
    // Verify that the specified package is SystemUI. You'll need to write your 
    // own logic to do this.
    if (isSystem(clientPackageName, clientUid)) {
        rootHints?.let {
            if (it.getBoolean(BrowserRoot.EXTRA_RECENT)) {
                // Return a tree with a single playable media item for resumption.
                val extras = Bundle().apply {
                    putBoolean(BrowserRoot.EXTRA_RECENT, true)
                }
                return BrowserRoot(MY_RECENTS_ROOT_ID, extras)
            }
        }
        // You can return your normal tree if the EXTRA_RECENT flag is not present.
        return BrowserRoot(MY_MEDIA_ROOT_ID, null)
    }
    // Return an empty tree to disallow browsing.
    return BrowserRoot(MY_EMPTY_ROOT_ID, null)

Java

@Override
public BrowserRoot onGetRoot(String clientPackageName, int clientUid,
    Bundle rootHints) {
    ...
    // Verify that the specified package is SystemUI. You'll need to write your
    // own logic to do this.
    if (isSystem(clientPackageName, clientUid)) {
        if (rootHints != null) {
            if (rootHints.getBoolean(BrowserRoot.EXTRA_RECENT)) {
                // Return a tree with a single playable media item for resumption.
                Bundle extras = new Bundle();
                extras.putBoolean(BrowserRoot.EXTRA_RECENT, true);
                return new BrowserRoot(MY_RECENTS_ROOT_ID, extras);
            }
        }
        // You can return your normal tree if the EXTRA_RECENT flag is not present.
        return new BrowserRoot(MY_MEDIA_ROOT_ID, null);
    }
    // Return an empty tree to disallow browsing.
    return new BrowserRoot(MY_EMPTY_ROOT_ID, null);
}

Verhalten vor Android 13

Aus Gründen der Abwärtskompatibilität bietet die System-UI weiterhin ein alternatives Layout, das Benachrichtigungsaktionen für Apps verwendet, die nicht auf Android 13 aktualisiert werden oder keine PlaybackState-Informationen enthalten. Die Aktionsbuttons werden aus der Liste Notification.Action abgeleitet, die an die Benachrichtigung MediaStyle angehängt ist. Das System zeigt bis zu fünf Aktionen in der Reihenfolge an, in der sie hinzugefügt wurden. Im Kompaktmodus werden bis zu drei Schaltflächen angezeigt, die durch die an setShowActionsInCompactView() übergebenen Werte bestimmt werden.

Benutzerdefinierte Aktionen werden in der Reihenfolge platziert, in der sie dem PlaybackState hinzugefügt wurden.

Im folgenden Codebeispiel wird gezeigt, wie Sie der MediaStyle-Benachrichtigung Aktionen hinzufügen :

Kotlin

import androidx.core.app.NotificationCompat
import androidx.media3.session.MediaStyleNotificationHelper

var notification = NotificationCompat.Builder(context, CHANNEL_ID)
// Show controls on lock screen even when user hides sensitive content.
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setSmallIcon(R.drawable.ic_stat_player)
// Add media control buttons that invoke intents in your media service
.addAction(R.drawable.ic_prev, "Previous", prevPendingIntent) // #0
.addAction(R.drawable.ic_pause, "Pause", pausePendingIntent) // #1
.addAction(R.drawable.ic_next, "Next", nextPendingIntent) // #2
// Apply the media style template
.setStyle(MediaStyleNotificationHelper.MediaStyle(mediaSession)
.setShowActionsInCompactView(1 /* #1: pause button */))
.setContentTitle("Wonderful music")
.setContentText("My Awesome Band")
.setLargeIcon(albumArtBitmap)
.build()

Java

import androidx.core.app.NotificationCompat;
import androidx.media3.session.MediaStyleNotificationHelper;

NotificationCompat.Builder notification = new NotificationCompat.Builder(context, CHANNEL_ID)
// Show controls on lock screen even when user hides sensitive content.
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setSmallIcon(R.drawable.ic_stat_player)
// Add media control buttons that invoke intents in your media service
.addAction(R.drawable.ic_prev, "Previous", prevPendingIntent) // #0
.addAction(R.drawable.ic_pause, "Pause", pausePendingIntent) // #1
.addAction(R.drawable.ic_next, "Next", nextPendingIntent) // #2
// Apply the media style template
.setStyle(new MediaStyleNotificationHelper.MediaStyle(mediaSession)
.setShowActionsInCompactView(1 /* #1: pause button */))
.setContentTitle("Wonderful music")
.setContentText("My Awesome Band")
.setLargeIcon(albumArtBitmap)
.build();