פקדי מדיה

אמצעי הבקרה של המדיה ב-Android נמצאים ליד ההגדרות המהירות. הסשנים מכמה אפליקציות מסודרים בקרוסלה שאפשר להחליק בה. ברשימת הקרוסלה מוצגות פעילויות בסדר הזה:

  • שידורים חיים שמופעלים באופן מקומי בטלפון
  • שידורים מרחוק, כמו אלה שמזוהים במכשירים חיצוניים או בסשנים של Cast
  • סשנים קודמים שאפשר להמשיך מהם, לפי הסדר שבו הם הופעלו לאחרונה

החל מ-Android 13 (רמת API ‏33), כדי לוודא שהמשתמשים יוכלו לגשת למגוון רחב של אמצעי בקרה למדיה באפליקציות שמפעילות מדיה, לחצני הפעולה באמצעי הבקרה למדיה נגזרים ממצב Player.

כך תוכלו להציג קבוצה עקבית של אמצעי בקרה למדיה וחוויה משופרת של אמצעי בקרה למדיה במכשירים שונים.

באיור 1 מוצגות דוגמאות לאופן שבו המודעה מוצגת בטלפון ובטאבלט, בהתאמה.

אמצעי בקרה להפעלת מדיה, שמוצגים בטלפונים ובטאבלטים. בדוגמה מוצג טראק לדוגמה, שבו אפשר לראות איך הלחצנים עשויים להופיע
איור 1: אמצעי בקרה של מדיה בטלפונים ובטאבלטים

המערכת מציגה עד חמישה לחצני פעולה על סמך המצב של Player, כפי שמתואר בטבלה הבאה. במצב קומפקטי, מוצגים רק שלושת משבצות הפעולה הראשונות. ההתנהגות הזו תואמת לאופן שבו כפתורי המדיה מוצגים בפלטפורמות אחרות של Android, כמו Auto, ‏ Assistant ו-Wear OS.

משבצת קריטריונים פעולה
1 playWhenReady הוא שקרי או שמצב ההפעלה הנוכחי STATE_ENDED. Play
playWhenReady הוא true ו מצב ההפעלה הנוכחי הוא STATE_BUFFERING. סימן גרפי של טעינה מתבצעת
playWhenReady הוא true ו מצב ההפעלה הנוכחי הוא STATE_READY. השהיה
2 העדפות של כפתור המדיה כוללות כפתור בהתאמה אישית ל-CommandButton.SLOT_BACK בהתאמה אישית
פקודת Player COMMAND_SEEK_TO_PREVIOUS או COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM זמינה. הקודם
אין אפשרות להשתמש בכפתור בהתאמה אישית או באחת מהפקודות שמופיעות ברשימה. ריק
3 העדפות כפתורי המדיה כוללות כפתור בהתאמה אישית ל-CommandButton.SLOT_FORWARD בהתאמה אישית
פקודת Player COMMAND_SEEK_TO_NEXT או COMMAND_SEEK_TO_NEXT_MEDIA_ITEM זמינה. הבא
אין אפשרות להשתמש בכפתור בהתאמה אישית או באחת מהפקודות שמופיעות ברשימה. ריק
4 בהעדפות של לחצן המדיה יש לחצן מותאם אישית ל-CommandButton.SLOT_OVERFLOW שעדיין לא הוצב. בהתאמה אישית
5 בהעדפות של לחצן המדיה יש לחצן מותאם אישית ל-CommandButton.SLOT_OVERFLOW שעדיין לא הוצב. בהתאמה אישית

כפתורים מותאמים אישית של תפריט האפשרויות הנוספות מוצבים בסדר שבו הם נוספו להעדפות של כפתורי המדיה.

התאמה אישית של לחצני פקודות

כדי להתאים אישית את אמצעי הבקרה של המדיה במערכת באמצעות Jetpack Media3, אפשר להגדיר את העדפות לחצני המדיה של הסשן ואת הפקודות הזמינות של אמצעי הבקרה בהתאם:

  1. יוצרים MediaSession ומגדירים את ההעדפות של כפתורי המדיה עבור כפתורי פקודות מותאמים אישית.

  2. ב-MediaSession.Callback.onConnect(), מגדירים את הפקודות הזמינות של הבקרים, כולל פקודות בהתאמה אישית, ב-ConnectionResult, כדי לאשר את הבקרים.

  3. ב-MediaSession.Callback.onCustomCommand(), עליך להגיב לפקודה המותאמת אישית שהמשתמש בחר.

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);
    }
  }
}

MediaSessionמידע נוסף על הגדרת ה-MediaSession כדי שלקוחות כמו המערכת יוכלו להתחבר לאפליקציית המדיה

כשמטמיעים MediaSession באמצעות Jetpack Media3,‏ PlaybackState מתעדכן אוטומטית בהתאם לנגן המדיה. באופן דומה, כשמטמיעים MediaSessionService, הספרייה מפרסמת באופן אוטומטי MediaStyle התראה בשבילכם ודואגת שהיא תהיה עדכנית.

תגובה ללחצני פעולה

כשמשתמש מקיש על לחצן פעולה באמצעי הבקרה של המדיה במערכת, MediaController של המערכת שולח פקודת הפעלה אל MediaSession. הפקודות האלה מועברות מה-MediaSession אל הנגן. הפקודות שמוגדרות בממשק Player של Media3 מטופלות באופן אוטומטי על ידי סשן המדיה.

הוראות להוספת תגובה לפקודה מותאמת אישית מופיעות במאמר הוספת פקודות מותאמות אישית.

תמיכה בהמשך הפעלה של מדיה

המשתמשים יכולים להפעיל מחדש סשנים קודמים מהקרוסלה בלי להפעיל את האפליקציה. כשההפעלה מתחילה, המשתמשים יכולים ליצור אינטראקציה עם אמצעי הבקרה של המדיה כרגיל.

אפשר להפעיל או להשבית את התכונה של המשך ההפעלה באמצעות אפליקציית ההגדרות, באפשרויות סאונד > מדיה. המשתמש יכול גם לגשת להגדרות על ידי הקשה על סמל גלגל השיניים שמופיע אחרי החלקה בקרוסלה המורחבת.

‫Media3 מציעה ממשקי API כדי להקל על תמיכה בהמשך הפעלה של מדיה. הוראות להטמעה של התכונה הזו זמינות במסמכים בנושא המשך הפעלה באמצעות Media3.

שימוש בממשקי API מדור קודם של מדיה

בקטע הזה מוסבר איך לשלב את האפליקציה עם אמצעי הבקרה של המדיה במערכת באמצעות ממשקי MediaCompat API מדור קודם.

המערכת מאחזרת את הפרטים הבאים מMediaSessionMediaMetadata ומציגה אותם כשהם זמינים:

  • METADATA_KEY_ALBUM_ART_URI
  • METADATA_KEY_TITLE
  • METADATA_KEY_DISPLAY_TITLE
  • METADATA_KEY_ARTIST
  • METADATA_KEY_DURATION (אם משך הזמן לא מוגדר, סרגל ההתקדמות לא מופיע)

כדי לוודא שקיבלתם התראה תקפה ומדויקת על אמצעי בקרה להפעלת מדיה, צריך להגדיר את הערך של המטא-נתונים METADATA_KEY_TITLE או METADATA_KEY_DISPLAY_TITLE לשם המדיה שמופעלת כרגע.

בנגן המדיה מוצג הזמן שחלף מאז תחילת ההפעלה של פריט המדיה הנוכחי, וגם סרגל ניווט שמופה ל-MediaSession PlaybackState.

בנגן המדיה מוצגת ההתקדמות של פריט המדיה שמופעל כרגע, לצד סרגל ניווט שממופה ל-MediaSession PlaybackState. סרגל הדילוג מאפשר למשתמשים לשנות את המיקום ומציג את הזמן שחלף של פריט המדיה. כדי להפעיל את סרגל ההתקדמות, צריך להטמיע את PlaybackState.Builder#setActions ולכלול את ACTION_SEEK_TO.

משבצת פעולה קריטריונים
1 Play הסטטוס הנוכחי של PlaybackState הוא אחד מהבאים:
  • STATE_NONE
  • STATE_STOPPED
  • STATE_PAUSED
  • STATE_ERROR
סימן גרפי של טעינה מתבצעת הסטטוס הנוכחי של PlaybackState הוא אחד מהבאים:
  • STATE_CONNECTING
  • STATE_BUFFERING
השהיה המצב הנוכחי של PlaybackState הוא אף אחת מהאפשרויות שלמעלה.
2 הקודם PlaybackState פעולות כוללות ACTION_SKIP_TO_PREVIOUS.
בהתאמה אישית PlaybackState actions לא כולל את ACTION_SKIP_TO_PREVIOUS ו-PlaybackState custom actions כולל פעולה מותאמת אישית שעדיין לא הוצבה.
ריק PlaybackState extras כולל ערך בוליאני true למפתח SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV.
3 הבא PlaybackState פעולות כוללות ACTION_SKIP_TO_NEXT.
בהתאמה אישית PlaybackState actions לא כולל את ACTION_SKIP_TO_NEXT ו-PlaybackState custom actions כולל פעולה מותאמת אישית שעדיין לא הוצבה.
ריק PlaybackState extras כולל ערך בוליאני true למפתח SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT.
4 בהתאמה אישית PlaybackState פעולות בהתאמה אישית כוללות פעולה בהתאמה אישית שעדיין לא הוצבה.
5 בהתאמה אישית PlaybackState פעולות בהתאמה אישית כוללות פעולה בהתאמה אישית שעדיין לא הוצבה.

הוספת פעולות רגילות

בדוגמאות הקוד הבאות אפשר לראות איך מוסיפים פעולות רגילות PlaybackState ופעולות מותאמות אישית.

כדי להגדיר את הפעולות 'הפעלה', 'השהיה', 'הקודם' ו'הבא', צריך להגדיר אותן ב-PlaybackState של סשן המדיה.

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);

אם לא רוצים להוסיף לחצנים במשבצות הקודמת או הבאה, לא מוסיפים את ACTION_SKIP_TO_PREVIOUS או ACTION_SKIP_TO_NEXT, אלא מוסיפים תוספים לסשן:

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);

הוספת פעולות בהתאמה אישית

כדי להציג פעולות אחרות באמצעי הבקרה של המדיה, אפשר ליצור PlaybackStateCompat.CustomAction ולהוסיף אותו ל-PlaybackState. הפעולות האלה מוצגות לפי הסדר שבו הן נוספו.

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);

תגובה לפעולות של PlaybackState

כשמשתמש מקיש על לחצן, SystemUI משתמש ב-MediaController.TransportControls כדי לשלוח פקודה בחזרה אל MediaSession. צריך לרשום פונקציית קריאה חוזרת שיכולה להגיב לאירועים האלה בצורה נכונה.

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);
        }
    }
};

המשך הפעלה של המדיה

כדי שאפליקציית הנגן תופיע באזור ההגדרות המהירות, צריך ליצור MediaStyle התראה עם טוקן MediaSession תקין.

כדי להציג את הכותרת של ההתראה MediaStyle, משתמשים ב-NotificationBuilder.setContentTitle().

כדי להציג את סמל המותג בנגן המדיה, משתמשים בתג NotificationBuilder.setSmallIcon().

כדי לתמוך בהמשך הפעלה, האפליקציות צריכות להטמיע את MediaBrowserService ואת MediaSession. ב-MediaSession צריך להטמיע את הקריאה החוזרת onPlay().

הטמעה של MediaBrowserService

אחרי שהמכשיר מופעל, המערכת מחפשת את חמש האפליקציות האחרונות ששימשו להפעלת מדיה, ומספקת אמצעי בקרה שאפשר להשתמש בהם כדי להפעיל מחדש את ההפעלה מכל אפליקציה.

המערכת מנסה ליצור קשר עם MediaBrowserService באמצעות חיבור מ-SystemUI. האפליקציה צריכה לאפשר חיבורים כאלה, אחרת היא לא תוכל לתמוך בהמשך הפעלה.

אפשר לזהות ולאמת חיבורים מ-SystemUI באמצעות שם החבילה com.android.systemui והחתימה. המערכת SystemUI חתומה בחתימת הפלטפורמה. באפליקציית UAMP אפשר לראות דוגמה לאופן הבדיקה של חתימת הפלטפורמה.

כדי לתמוך בהמשך ההפעלה, צריך להטמיע את ההתנהגויות הבאות ב-MediaBrowserService:

  • הפונקציה onGetRoot() צריכה להחזיר שורש שאינו null במהירות. לוגיקה מורכבת אחרת צריכה להיות מטופלת ב-onLoadChildren()

  • כשקוראים לפונקציה onLoadChildren() במזהה המדיה הבסיסי, התוצאה צריכה להכיל צאצא FLAG_PLAYABLE.

  • הפונקציה MediaBrowserService אמורה להחזיר את פריט המדיה שהושמע לאחרונה כשמתקבלת שאילתה מסוג EXTRA_RECENT. הערך שמוחזר צריך להיות פריט מדיה בפועל ולא פונקציה כללית.

  • MediaBrowserService צריך לספק MediaDescription מתאים עם title ו-subtitle לא ריקים. צריך גם להגדיר מזהה URI של סמל או מפת סיביות של סמל.

בדוגמאות הקוד הבאות אפשר לראות איך מטמיעים את onGetRoot().

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);
}

התנהגות בגרסאות Android קודמות ל-Android 13

לצורך תאימות לאחור, ממשק המשתמש של המערכת ממשיך לספק פריסה חלופית שמשתמשת בפעולות של התראות באפליקציות שלא מעדכנות ל-Android 13 או שלא כוללות מידע על PlaybackState. לחצני הפעולה נגזרים מרשימת Notification.Action שמצורפת להתראה MediaStyle. המערכת מציגה עד חמש פעולות לפי הסדר שבו הן נוספו. במצב קומפקטי, מוצגים עד שלושה לחצנים, בהתאם לערכים שמועברים אל setShowActionsInCompactView().

הפעולות המותאמות אישית ממוקמות לפי הסדר שבו הן נוספו ל-PlaybackState.

בדוגמת הקוד הבאה אפשר לראות איך מוסיפים פעולות להתראה MediaStyle :

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();