public abstract class

MediaSessionService

extends Service

 java.lang.Object

↳Service

↳androidx.media3.session.MediaSessionService

Subclasses:

MediaLibraryService

Gradle dependencies

compile group: 'androidx.media3', name: 'media3-session', version: '1.5.0-alpha01'

  • groupId: androidx.media3
  • artifactId: media3-session
  • version: 1.5.0-alpha01

Artifact androidx.media3:media3-session:1.5.0-alpha01 it located at Google repository (https://maven.google.com/)

Overview

Superclass to be extended by services hosting media sessions.

It's highly recommended for an app to use this class if media playback should continue while in the background. The service allows other apps to know that your app supports MediaSession even when your app isn't running. This way, a user voice command may be able start your app to play media.

To extend this class, declare the intent filter in your AndroidManifest.xml:

 
   
     
   
 
 

You may also declare the action android.media.browse.MediaBrowserService for compatibility with android.support.v4.media.MediaBrowserCompat. This service can handle the case automatically.

It's recommended for an app to have a single service declared in the manifest. Otherwise, your app might be shown twice in the list of the controller apps, or another app might fail to pick the right service when it wants to start a playback on this app. If you want to provide multiple sessions, take a look at Supporting Multiple Sessions.

Topics covered here:

  1. Service Lifecycle
  2. Supporting Multiple Sessions

Service Lifecycle

A media session service is a bound service and its foreground service type must include mediaPlayback. When a MediaController is created for the service, the controller binds to the service. MediaSessionService will be called from MediaSessionService.onBind(Intent).

After binding, the session's MediaSession.Callback.onConnect(MediaSession, MediaSession.ControllerInfo) will be called to accept or reject the connection request from the controller. If it's accepted, the controller will be available and keep the binding. If it's rejected, the controller will unbind.

MediaSessionService.onUpdateNotification(MediaSession, boolean) will be called whenever a notification needs to be shown, updated or cancelled. The default implementation will display notifications using a default UI or using a MediaNotification.Provider that's set with MediaSessionService.setMediaNotificationProvider(MediaNotification.Provider). In addition, when playback starts, the service will become a foreground service. It's required to keep the playback after the controller is destroyed. The service will become a background service when all playbacks are stopped. Apps targeting SDK_INT >= 28 must request the permission, , in order to make the service foreground. You can control when to show or hide notifications by overriding MediaSessionService.onUpdateNotification(MediaSession, boolean). In this case, you must also start or stop the service from the foreground, when playback starts or stops respectively.

The service will be destroyed when all sessions are released, or no controller is binding to the service while the service is in the background.

Supporting Multiple Sessions

Generally, multiple sessions aren't necessary for most media apps. One exception is if your app can play multiple media contents at the same time, but only for playback of video-only media or remote playback, since the audio focus policy recommends not playing multiple audio contents at the same time. Also, keep in mind that multiple media sessions would make Android Auto and Bluetooth devices with a display to show your apps multiple times, because they list up media sessions, not media apps.

However, if you're capable of handling multiple playbacks and want to keep their sessions while the app is in the background, create multiple sessions and add them to this service with MediaSessionService.addSession(MediaSession).

Note that a MediaController can be created with SessionToken to connect to a session in this service. In that case, MediaSessionService will be called to decide which session to handle the connection request. Pick the best session among the added sessions, or create a new session and return it from MediaSessionService.

Summary

Fields
public static final java.lang.StringSERVICE_INTERFACE

The action for filter that must be declared by the service.

Constructors
publicMediaSessionService()

Creates a service.

Methods
public final voidaddSession(MediaSession session)

Adds a MediaSession to this service.

public final voidclearListener()

Clears the listener.

public final java.util.List<MediaSession>getSessions()

Returns the list of sessions that you've added to this service via MediaSessionService.addSession(MediaSession) or MediaSessionService.

public booleanisPlaybackOngoing()

Returns whether there is a session with ongoing playback that must be paused or stopped before being able to terminate the service by calling MediaSessionService.

public final booleanisSessionAdded(MediaSession session)

Returns whether session has been added to this service via MediaSessionService.addSession(MediaSession) or MediaSessionService.

public IBinderonBind(Intent intent)

Called when a component is about to bind to the service.

public voidonCreate()

Called when the service is created.

public voidonDestroy()

Called when the service is no longer used and is being removed.

public abstract MediaSessiononGetSession(MediaSession.ControllerInfo controllerInfo)

Called when a MediaController is created with this service's SessionToken.

public intonStartCommand(Intent intent, int flags, int startId)

Called when a component calls .

public voidonTaskRemoved(Intent rootIntent)

public voidonUpdateNotification(MediaSession session)

public voidonUpdateNotification(MediaSession session, boolean startInForegroundRequired)

Called when a notification needs to be updated.

public voidpauseAllPlayersAndStopSelf()

Pauses the player of each session managed by the service and calls MediaSessionService.

public final voidremoveSession(MediaSession session)

Removes a MediaSession from this service.

public final voidsetListener(MediaSessionService.Listener listener)

Sets the listener.

protected final voidsetMediaNotificationProvider(MediaNotification.Provider mediaNotificationProvider)

Sets the MediaNotification.Provider to customize notifications.

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Fields

public static final java.lang.String SERVICE_INTERFACE

The action for filter that must be declared by the service.

Constructors

public MediaSessionService()

Creates a service.

Methods

public void onCreate()

Called when the service is created.

Override this method if you need your own initialization.

This method will be called on the main thread.

public abstract MediaSession onGetSession(MediaSession.ControllerInfo controllerInfo)

Called when a MediaController is created with this service's SessionToken. Return a MediaSession that the controller will connect to, or null to reject the connection request.

The service automatically maintains the returned sessions. In other words, a session returned by this method will be added to the service, and removed from the service when the session is closed. You don't need to manually call MediaSessionService.addSession(MediaSession) nor MediaSessionService.removeSession(MediaSession).

There are two special cases where the MediaSession.ControllerInfo.getPackageName() returns a non-existent package name:

  • When the service is started by a media button event, the package name will be . If you want to allow the service to be started by media button events, do not return null.
  • When a legacy or a android.support.v4.media.MediaBrowserCompat tries to connect, the package name will be . If you want to allow the service to be bound by the legacy media browsers, do not return null.

For those special cases, the values returned by MediaSession.ControllerInfo.getUid() and MediaSession.ControllerInfo.getConnectionHints() have no meaning.

This method will be called on the main thread.

Parameters:

controllerInfo: The information of the controller that is trying to connect.

Returns:

A MediaSession for the controller, or null to reject the connection.

See also: MediaSession.Builder, MediaSessionService.getSessions()

public final void addSession(MediaSession session)

Adds a MediaSession to this service. This is not necessary for most media apps. See Supporting Multiple Sessions for details.

The added session will be removed automatically when the session is released.

This method can be called from any thread.

Parameters:

session: A session to be added.

See also: MediaSessionService.removeSession(MediaSession), MediaSessionService.getSessions()

public final void removeSession(MediaSession session)

Removes a MediaSession from this service. This is not necessary for most media apps. See Supporting Multiple Sessions for details.

This method can be called from any thread.

Parameters:

session: A session to be removed.

See also: MediaSessionService.addSession(MediaSession), MediaSessionService.getSessions()

public final java.util.List<MediaSession> getSessions()

Returns the list of sessions that you've added to this service via MediaSessionService.addSession(MediaSession) or MediaSessionService.

This method can be called from any thread.

public final boolean isSessionAdded(MediaSession session)

Returns whether session has been added to this service via MediaSessionService.addSession(MediaSession) or MediaSessionService.

This method can be called from any thread.

public final void setListener(MediaSessionService.Listener listener)

Sets the listener.

This method can be called from any thread.

public final void clearListener()

Clears the listener.

This method can be called from any thread.

public IBinder onBind(Intent intent)

Called when a component is about to bind to the service.

The default implementation handles the incoming requests from controllers. In this case, the intent will have the action MediaSessionService.SERVICE_INTERFACE. Override this method if this service also needs to handle actions other than MediaSessionService.SERVICE_INTERFACE.

This method will be called on the main thread.

public int onStartCommand(Intent intent, int flags, int startId)

Called when a component calls .

The default implementation handles the incoming media button events. In this case, the intent will have the action . Override this method if this service also needs to handle actions other than .

This method will be called on the main thread.

public boolean isPlaybackOngoing()

Returns whether there is a session with ongoing playback that must be paused or stopped before being able to terminate the service by calling MediaSessionService.

public void pauseAllPlayersAndStopSelf()

Pauses the player of each session managed by the service and calls MediaSessionService.

This terminates the service lifecycle and triggers MediaSessionService.onDestroy() that an app can override to release the sessions and other resources.

public void onTaskRemoved(Intent rootIntent)

If playback is ongoing, the service continues running in the foreground when the app is dismissed from the recent apps. Otherwise, the service is stopped by calling MediaSessionService which terminates the service lifecycle and triggers MediaSessionService.onDestroy() that an app can override to release the sessions and other resources.

An app can safely override this method without calling super to implement a different behaviour, for instance unconditionally calling MediaSessionService.pauseAllPlayersAndStopSelf() to stop the service even when playing. However, if playback is not ongoing, the service must be terminated otherwise the service will be crashed and restarted by the system.

Note: The service can't be stopped until all media controllers have been unbound. Hence, an app needs to release all internal controllers that have connected to the service (for instance from an activity in ). If an app allows external apps to connect a MediaController to the service, these controllers also need to be disconnected. In such a scenario of external bound clients, an app needs to override this method to release the session before calling MediaSessionService.

public void onDestroy()

Called when the service is no longer used and is being removed.

Override this method if you need your own clean up.

This method will be called on the main thread.

public void onUpdateNotification(MediaSession session)

Deprecated: Use MediaSessionService.onUpdateNotification(MediaSession, boolean) instead.

public void onUpdateNotification(MediaSession session, boolean startInForegroundRequired)

Called when a notification needs to be updated. Override this method to show or cancel your own notifications.

This method is called whenever the service has detected a change that requires to show, update or cancel a notification with a flag startInForegroundRequired suggested by the service whether starting in the foreground is required. The method will be called on the application thread of the app that the service belongs to.

Override this method to create your own notification and customize the foreground handling of your service.

The default implementation will present a default notification or the notification provided by the MediaNotification.Provider that is set by the app. Further, the service is started in the foreground when playback is ongoing and put back into background otherwise.

Apps targeting SDK_INT >= 28 must request the permission, .

This method will be called on the main thread.

Parameters:

session: A session that needs notification update.
startInForegroundRequired: Whether the service is required to start in the foreground.

protected final void setMediaNotificationProvider(MediaNotification.Provider mediaNotificationProvider)

Sets the MediaNotification.Provider to customize notifications.

This should be called before MediaSessionService.onCreate() returns.

This method can be called from any thread.

Source

/*
 * Copyright 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package androidx.media3.session;

import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import static androidx.media3.common.util.Util.postOrRun;

import android.app.Activity;
import android.app.ForegroundServiceStartNotAllowedException;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import androidx.annotation.CallSuper;
import androidx.annotation.DoNotInline;
import androidx.annotation.GuardedBy;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.collection.ArrayMap;
import androidx.media3.common.MediaLibraryInfo;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.session.MediaSession.ControllerInfo;
import androidx.media3.session.legacy.MediaBrowserServiceCompat;
import androidx.media3.session.legacy.MediaSessionManager;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;

/**
 * Superclass to be extended by services hosting {@link MediaSession media sessions}.
 *
 * <p>It's highly recommended for an app to use this class if media playback should continue while
 * in the background. The service allows other apps to know that your app supports {@link
 * MediaSession} even when your app isn't running. This way, a user voice command may be able start
 * your app to play media.
 *
 * <p>To extend this class, declare the intent filter in your {@code AndroidManifest.xml}:
 *
 * <pre>{@code
 * <service
 *   android:name="NameOfYourService"
 *   android:foregroundServiceType="mediaPlayback"
 *   android:exported="true">
 *   <intent-filter>
 *     <action android:name="androidx.media3.session.MediaSessionService"/>
 *   </intent-filter>
 * </service>
 * }</pre>
 *
 * <p>You may also declare the action {@code android.media.browse.MediaBrowserService} for
 * compatibility with {@code android.support.v4.media.MediaBrowserCompat}. This service can handle
 * the case automatically.
 *
 * <p>It's recommended for an app to have a single service declared in the manifest. Otherwise, your
 * app might be shown twice in the list of the controller apps, or another app might fail to pick
 * the right service when it wants to start a playback on this app. If you want to provide multiple
 * sessions, take a look at <a href="#MultipleSessions">Supporting Multiple Sessions</a>.
 *
 * <p>Topics covered here:
 *
 * <ol>
 *   <li><a href="#ServiceLifecycle">Service Lifecycle</a>
 *   <li><a href="#MultipleSessions">Supporting Multiple Sessions</a>
 * </ol>
 *
 * <h2 id="ServiceLifecycle">Service Lifecycle</h2>
 *
 * <p>A media session service is a bound service and its <a
 * href="https://developer.android.com/guide/topics/manifest/service-element#foregroundservicetype">
 * foreground service type</a> must include <em>mediaPlayback</em>. When a {@link MediaController}
 * is created for the service, the controller binds to the service. {@link
 * #onGetSession(ControllerInfo)} will be called from {@link #onBind(Intent)}.
 *
 * <p>After binding, the session's {@link MediaSession.Callback#onConnect(MediaSession,
 * MediaSession.ControllerInfo)} will be called to accept or reject the connection request from the
 * controller. If it's accepted, the controller will be available and keep the binding. If it's
 * rejected, the controller will unbind.
 *
 * <p>{@link #onUpdateNotification(MediaSession, boolean)} will be called whenever a notification
 * needs to be shown, updated or cancelled. The default implementation will display notifications
 * using a default UI or using a {@link MediaNotification.Provider} that's set with {@link
 * #setMediaNotificationProvider}. In addition, when playback starts, the service will become a <a
 * href="https://developer.android.com/guide/components/foreground-services">foreground service</a>.
 * It's required to keep the playback after the controller is destroyed. The service will become a
 * background service when all playbacks are stopped. Apps targeting {@code SDK_INT >= 28} must
 * request the permission, {@link android.Manifest.permission#FOREGROUND_SERVICE}, in order to make
 * the service foreground. You can control when to show or hide notifications by overriding {@link
 * #onUpdateNotification(MediaSession, boolean)}. In this case, you must also start or stop the
 * service from the foreground, when playback starts or stops respectively.
 *
 * <p>The service will be destroyed when all sessions are {@linkplain MediaController#release()
 * released}, or no controller is binding to the service while the service is in the background.
 *
 * <h2 id="MultipleSessions">Supporting Multiple Sessions</h2>
 *
 * <p>Generally, multiple sessions aren't necessary for most media apps. One exception is if your
 * app can play multiple media contents at the same time, but only for playback of video-only media
 * or remote playback, since the <a
 * href="https://developer.android.com/media/optimize/audio-focus">audio focus policy</a> recommends
 * not playing multiple audio contents at the same time. Also, keep in mind that multiple media
 * sessions would make Android Auto and Bluetooth devices with a display to show your apps multiple
 * times, because they list up media sessions, not media apps.
 *
 * <p>However, if you're capable of handling multiple playbacks and want to keep their sessions
 * while the app is in the background, create multiple sessions and add them to this service with
 * {@link #addSession(MediaSession)}.
 *
 * <p>Note that a {@link MediaController} can be created with {@link SessionToken} to connect to a
 * session in this service. In that case, {@link #onGetSession(ControllerInfo)} will be called to
 * decide which session to handle the connection request. Pick the best session among the added
 * sessions, or create a new session and return it from {@link #onGetSession(ControllerInfo)}.
 */
public abstract class MediaSessionService extends Service {

  /**
   * Listener for {@link MediaSessionService}.
   *
   * <p>The methods will be called on the main thread.
   */
  @UnstableApi
  public interface Listener {
    /**
     * Called when the service fails to start in the foreground and a {@link
     * ForegroundServiceStartNotAllowedException} is thrown on Android 12 or later.
     */
    @RequiresApi(31)
    default void onForegroundServiceStartNotAllowedException() {}
  }

  /** The action for {@link Intent} filter that must be declared by the service. */
  public static final String SERVICE_INTERFACE = "androidx.media3.session.MediaSessionService";

  private static final String TAG = "MSessionService";

  private final Object lock;
  private final Handler mainHandler;

  @GuardedBy("lock")
  private final Map<String, MediaSession> sessions;

  @GuardedBy("lock")
  @Nullable
  private MediaSessionServiceStub stub;

  @GuardedBy("lock")
  private @MonotonicNonNull MediaNotificationManager mediaNotificationManager;

  @GuardedBy("lock")
  private MediaNotification.@MonotonicNonNull Provider mediaNotificationProvider;

  @GuardedBy("lock")
  private @MonotonicNonNull DefaultActionFactory actionFactory;

  @GuardedBy("lock")
  @Nullable
  private Listener listener;

  private boolean defaultMethodCalled;

  /** Creates a service. */
  public MediaSessionService() {
    lock = new Object();
    mainHandler = new Handler(Looper.getMainLooper());
    sessions = new ArrayMap<>();
    defaultMethodCalled = false;
  }

  /**
   * Called when the service is created.
   *
   * <p>Override this method if you need your own initialization.
   *
   * <p>This method will be called on the main thread.
   */
  @CallSuper
  @Override
  public void onCreate() {
    super.onCreate();
    synchronized (lock) {
      stub = new MediaSessionServiceStub(this);
    }
  }

  /**
   * Called when a {@link MediaController} is created with this service's {@link SessionToken}.
   * Return a {@link MediaSession} that the controller will connect to, or {@code null} to reject
   * the connection request.
   *
   * <p>The service automatically maintains the returned sessions. In other words, a session
   * returned by this method will be added to the service, and removed from the service when the
   * session is closed. You don't need to manually call {@link #addSession(MediaSession)} nor {@link
   * #removeSession(MediaSession)}.
   *
   * <p>There are two special cases where the {@link ControllerInfo#getPackageName()} returns a
   * non-existent package name:
   *
   * <ul>
   *   <li>When the service is started by a media button event, the package name will be {@link
   *       Intent#ACTION_MEDIA_BUTTON}. If you want to allow the service to be started by media
   *       button events, do not return {@code null}.
   *   <li>When a legacy {@link android.media.browse.MediaBrowser} or a {@code
   *       android.support.v4.media.MediaBrowserCompat} tries to connect, the package name will be
   *       {@link android.service.media.MediaBrowserService#SERVICE_INTERFACE}. If you want to allow
   *       the service to be bound by the legacy media browsers, do not return {@code null}.
   * </ul>
   *
   * <p>For those special cases, the values returned by {@link ControllerInfo#getUid()} and {@link
   * ControllerInfo#getConnectionHints()} have no meaning.
   *
   * <p>This method will be called on the main thread.
   *
   * @param controllerInfo The information of the controller that is trying to connect.
   * @return A {@link MediaSession} for the controller, or {@code null} to reject the connection.
   * @see MediaSession.Builder
   * @see #getSessions()
   */
  @Nullable
  public abstract MediaSession onGetSession(ControllerInfo controllerInfo);

  /**
   * Adds a {@link MediaSession} to this service. This is not necessary for most media apps. See <a
   * href="#MultipleSessions">Supporting Multiple Sessions</a> for details.
   *
   * <p>The added session will be removed automatically {@linkplain MediaSession#release() when the
   * session is released}.
   *
   * <p>This method can be called from any thread.
   *
   * @param session A session to be added.
   * @see #removeSession(MediaSession)
   * @see #getSessions()
   */
  public final void addSession(MediaSession session) {
    checkNotNull(session, "session must not be null");
    checkArgument(!session.isReleased(), "session is already released");
    @Nullable MediaSession old;
    synchronized (lock) {
      old = sessions.get(session.getId());
      checkArgument(old == null || old == session, "Session ID should be unique");
      sessions.put(session.getId(), session);
    }
    if (old == null) {
      // Session has returned for the first time. Register callbacks.
      // TODO(b/191644474): Check whether the session is registered to multiple services.
      MediaNotificationManager notificationManager = getMediaNotificationManager();
      postOrRun(
          mainHandler,
          () -> {
            notificationManager.addSession(session);
            session.setListener(new MediaSessionListener());
          });
    }
  }

  /**
   * Removes a {@link MediaSession} from this service. This is not necessary for most media apps.
   * See <a href="#MultipleSessions">Supporting Multiple Sessions</a> for details.
   *
   * <p>This method can be called from any thread.
   *
   * @param session A session to be removed.
   * @see #addSession(MediaSession)
   * @see #getSessions()
   */
  public final void removeSession(MediaSession session) {
    checkNotNull(session, "session must not be null");
    synchronized (lock) {
      checkArgument(sessions.containsKey(session.getId()), "session not found");
      sessions.remove(session.getId());
    }
    MediaNotificationManager notificationManager = getMediaNotificationManager();
    postOrRun(
        mainHandler,
        () -> {
          notificationManager.removeSession(session);
          session.clearListener();
        });
  }

  /**
   * Returns the list of {@linkplain MediaSession sessions} that you've added to this service via
   * {@link #addSession} or {@link #onGetSession(ControllerInfo)}.
   *
   * <p>This method can be called from any thread.
   */
  public final List<MediaSession> getSessions() {
    synchronized (lock) {
      return new ArrayList<>(sessions.values());
    }
  }

  /**
   * Returns whether {@code session} has been added to this service via {@link #addSession} or
   * {@link #onGetSession(ControllerInfo)}.
   *
   * <p>This method can be called from any thread.
   */
  public final boolean isSessionAdded(MediaSession session) {
    synchronized (lock) {
      return sessions.containsKey(session.getId());
    }
  }

  /**
   * Sets the {@linkplain Listener listener}.
   *
   * <p>This method can be called from any thread.
   */
  @UnstableApi
  public final void setListener(Listener listener) {
    synchronized (lock) {
      this.listener = listener;
    }
  }

  /**
   * Clears the {@linkplain Listener listener}.
   *
   * <p>This method can be called from any thread.
   */
  @UnstableApi
  public final void clearListener() {
    synchronized (lock) {
      this.listener = null;
    }
  }

  /**
   * Called when a component is about to bind to the service.
   *
   * <p>The default implementation handles the incoming requests from {@link MediaController
   * controllers}. In this case, the intent will have the action {@link #SERVICE_INTERFACE}.
   * Override this method if this service also needs to handle actions other than {@link
   * #SERVICE_INTERFACE}.
   *
   * <p>This method will be called on the main thread.
   */
  @CallSuper
  @Override
  @Nullable
  public IBinder onBind(@Nullable Intent intent) {
    if (intent == null) {
      return null;
    }
    @Nullable String action = intent.getAction();
    if (action == null) {
      return null;
    }
    switch (action) {
      case MediaSessionService.SERVICE_INTERFACE:
        return getServiceBinder();
      case MediaBrowserServiceCompat.SERVICE_INTERFACE:
        {
          ControllerInfo controllerInfo = ControllerInfo.createLegacyControllerInfo();
          @Nullable MediaSession session = onGetSession(controllerInfo);
          if (session == null) {
            // Legacy MediaBrowser(Compat) cannot connect to this service.
            return null;
          }
          addSession(session);
          // Return a specific session's legacy binder although the Android framework caches
          // the returned binder here and next binding request may reuse cached binder even
          // after the session is closed.
          // Disclaimer: Although MediaBrowserCompat can only get the session that initially
          // set, it doesn't make things bad. Such limitation had been there between
          // MediaBrowserCompat and MediaBrowserServiceCompat.
          return session.getLegacyBrowserServiceBinder();
        }
      default:
        return null;
    }
  }

  /**
   * Called when a component calls {@link android.content.Context#startService(Intent)}.
   *
   * <p>The default implementation handles the incoming media button events. In this case, the
   * intent will have the action {@link Intent#ACTION_MEDIA_BUTTON}. Override this method if this
   * service also needs to handle actions other than {@link Intent#ACTION_MEDIA_BUTTON}.
   *
   * <p>This method will be called on the main thread.
   */
  @CallSuper
  @Override
  public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
    if (intent == null) {
      return START_STICKY;
    }

    DefaultActionFactory actionFactory = getActionFactory();
    @Nullable Uri uri = intent.getData();
    @Nullable MediaSession session = uri != null ? MediaSession.getSession(uri) : null;
    if (actionFactory.isMediaAction(intent)) {
      if (session == null) {
        ControllerInfo controllerInfo = ControllerInfo.createLegacyControllerInfo();
        session = onGetSession(controllerInfo);
        if (session == null) {
          return START_STICKY;
        }
        addSession(session);
      }
      MediaSessionImpl sessionImpl = session.getImpl();
      sessionImpl
          .getApplicationHandler()
          .post(
              () -> {
                ControllerInfo callerInfo = sessionImpl.getMediaNotificationControllerInfo();
                if (callerInfo == null) {
                  callerInfo = createFallbackMediaButtonCaller(intent);
                }
                if (!sessionImpl.onMediaButtonEvent(callerInfo, intent)) {
                  Log.d(TAG, "Ignored unrecognized media button intent.");
                }
              });
    } else if (session != null && actionFactory.isCustomAction(intent)) {
      @Nullable String customAction = actionFactory.getCustomAction(intent);
      if (customAction == null) {
        return START_STICKY;
      }
      Bundle customExtras = actionFactory.getCustomActionExtras(intent);
      getMediaNotificationManager().onCustomAction(session, customAction, customExtras);
    }
    return START_STICKY;
  }

  private static ControllerInfo createFallbackMediaButtonCaller(Intent mediaButtonIntent) {
    @Nullable ComponentName componentName = mediaButtonIntent.getComponent();
    String packageName =
        componentName != null
            ? componentName.getPackageName()
            : "androidx.media3.session.MediaSessionService";
    return new ControllerInfo(
        new MediaSessionManager.RemoteUserInfo(
            packageName,
            MediaSessionManager.RemoteUserInfo.UNKNOWN_PID,
            MediaSessionManager.RemoteUserInfo.UNKNOWN_UID),
        MediaLibraryInfo.VERSION_INT,
        MediaControllerStub.VERSION_INT,
        /* trusted= */ false,
        /* cb= */ null,
        /* connectionHints= */ Bundle.EMPTY);
  }

  /**
   * Returns whether there is a session with ongoing playback that must be paused or stopped before
   * being able to terminate the service by calling {@link #stopSelf()}.
   */
  @UnstableApi
  public boolean isPlaybackOngoing() {
    return getMediaNotificationManager().isStartedInForeground();
  }

  /**
   * Pauses the player of each session managed by the service and calls {@link #stopSelf()}.
   *
   * <p>This terminates the service lifecycle and triggers {@link #onDestroy()} that an app can
   * override to release the sessions and other resources.
   */
  @UnstableApi
  public void pauseAllPlayersAndStopSelf() {
    List<MediaSession> sessionList = getSessions();
    for (int i = 0; i < sessionList.size(); i++) {
      sessionList.get(i).getPlayer().setPlayWhenReady(false);
    }
    stopSelf();
  }

  /**
   * {@inheritDoc}
   *
   * <p>If {@linkplain #isPlaybackOngoing() playback is ongoing}, the service continues running in
   * the foreground when the app is dismissed from the recent apps. Otherwise, the service is
   * stopped by calling {@link #stopSelf()} which terminates the service lifecycle and triggers
   * {@link #onDestroy()} that an app can override to release the sessions and other resources.
   *
   * <p>An app can safely override this method without calling super to implement a different
   * behaviour, for instance unconditionally calling {@link #pauseAllPlayersAndStopSelf()} to stop
   * the service even when playing. However, if {@linkplain #isPlaybackOngoing() playback is not
   * ongoing}, the service must be terminated otherwise the service will be crashed and restarted by
   * the system.
   *
   * <p>Note: The service <a
   * href="https://developer.android.com/develop/background-work/services/bound-services#Lifecycle">can't
   * be stopped</a> until all media controllers have been unbound. Hence, an app needs to release
   * all internal controllers that have connected to the service (for instance from an activity in
   * {@link Activity#onStop()}). If an app allows external apps to connect a {@link MediaController}
   * to the service, these controllers also need to be disconnected. In such a scenario of external
   * bound clients, an app needs to override this method to release the session before calling
   * {@link #stopSelf()}.
   */
  @Override
  public void onTaskRemoved(@Nullable Intent rootIntent) {
    if (!isPlaybackOngoing()) {
      // The service needs to be stopped when playback is not ongoing and the service is not in the
      // foreground.
      stopSelf();
    }
  }

  /**
   * Called when the service is no longer used and is being removed.
   *
   * <p>Override this method if you need your own clean up.
   *
   * <p>This method will be called on the main thread.
   */
  @CallSuper
  @Override
  public void onDestroy() {
    super.onDestroy();
    synchronized (lock) {
      if (stub != null) {
        stub.release();
        stub = null;
      }
    }
  }

  /**
   * @deprecated Use {@link #onUpdateNotification(MediaSession, boolean)} instead.
   */
  @Deprecated
  public void onUpdateNotification(MediaSession session) {
    defaultMethodCalled = true;
  }

  /**
   * Called when a notification needs to be updated. Override this method to show or cancel your own
   * notifications.
   *
   * <p>This method is called whenever the service has detected a change that requires to show,
   * update or cancel a notification with a flag {@code startInForegroundRequired} suggested by the
   * service whether starting in the foreground is required. The method will be called on the
   * application thread of the app that the service belongs to.
   *
   * <p>Override this method to create your own notification and customize the foreground handling
   * of your service.
   *
   * <p>The default implementation will present a default notification or the notification provided
   * by the {@link MediaNotification.Provider} that is {@link
   * #setMediaNotificationProvider(MediaNotification.Provider) set} by the app. Further, the service
   * is started in the <a
   * href="https://developer.android.com/guide/components/foreground-services">foreground</a> when
   * playback is ongoing and put back into background otherwise.
   *
   * <p>Apps targeting {@code SDK_INT >= 28} must request the permission, {@link
   * android.Manifest.permission#FOREGROUND_SERVICE}.
   *
   * <p>This method will be called on the main thread.
   *
   * @param session A session that needs notification update.
   * @param startInForegroundRequired Whether the service is required to start in the foreground.
   */
  @SuppressWarnings("deprecation") // Calling deprecated method.
  public void onUpdateNotification(MediaSession session, boolean startInForegroundRequired) {
    onUpdateNotification(session);
    if (defaultMethodCalled) {
      getMediaNotificationManager().updateNotification(session, startInForegroundRequired);
    }
  }

  /**
   * Sets the {@link MediaNotification.Provider} to customize notifications.
   *
   * <p>This should be called before {@link #onCreate()} returns.
   *
   * <p>This method can be called from any thread.
   */
  @UnstableApi
  protected final void setMediaNotificationProvider(
      MediaNotification.Provider mediaNotificationProvider) {
    checkNotNull(mediaNotificationProvider);
    synchronized (lock) {
      this.mediaNotificationProvider = mediaNotificationProvider;
    }
  }

  /* package */ IBinder getServiceBinder() {
    synchronized (lock) {
      return checkStateNotNull(stub).asBinder();
    }
  }

  /**
   * Triggers notification update and handles {@code ForegroundServiceStartNotAllowedException}.
   *
   * <p>This method will be called on the main thread.
   */
  /* package */ boolean onUpdateNotificationInternal(
      MediaSession session, boolean startInForegroundWhenPaused) {
    try {
      boolean startInForegroundRequired =
          getMediaNotificationManager().shouldRunInForeground(session, startInForegroundWhenPaused);
      onUpdateNotification(session, startInForegroundRequired);
    } catch (/* ForegroundServiceStartNotAllowedException */ IllegalStateException e) {
      if ((Util.SDK_INT >= 31) && Api31.instanceOfForegroundServiceStartNotAllowedException(e)) {
        Log.e(TAG, "Failed to start foreground", e);
        onForegroundServiceStartNotAllowedException();
        return false;
      }
      throw e;
    }
    return true;
  }

  private MediaNotificationManager getMediaNotificationManager() {
    synchronized (lock) {
      if (mediaNotificationManager == null) {
        if (mediaNotificationProvider == null) {
          mediaNotificationProvider =
              new DefaultMediaNotificationProvider.Builder(getApplicationContext()).build();
        }
        mediaNotificationManager =
            new MediaNotificationManager(
                /* mediaSessionService= */ this, mediaNotificationProvider, getActionFactory());
      }
      return mediaNotificationManager;
    }
  }

  private DefaultActionFactory getActionFactory() {
    synchronized (lock) {
      if (actionFactory == null) {
        actionFactory = new DefaultActionFactory(/* service= */ this);
      }
      return actionFactory;
    }
  }

  @Nullable
  private Listener getListener() {
    synchronized (lock) {
      return this.listener;
    }
  }

  @RequiresApi(31)
  private void onForegroundServiceStartNotAllowedException() {
    mainHandler.post(
        () -> {
          @Nullable MediaSessionService.Listener serviceListener = getListener();
          if (serviceListener != null) {
            serviceListener.onForegroundServiceStartNotAllowedException();
          }
        });
  }

  private final class MediaSessionListener implements MediaSession.Listener {

    @Override
    public void onNotificationRefreshRequired(MediaSession session) {
      MediaSessionService.this.onUpdateNotificationInternal(
          session, /* startInForegroundWhenPaused= */ false);
    }

    @Override
    public boolean onPlayRequested(MediaSession session) {
      if (Util.SDK_INT < 31 || Util.SDK_INT >= 33) {
        return true;
      }
      // Check if service can start foreground successfully on Android 12 and 12L.
      if (!getMediaNotificationManager().isStartedInForeground()) {
        return onUpdateNotificationInternal(session, /* startInForegroundWhenPaused= */ true);
      }
      return true;
    }
  }

  private static final class MediaSessionServiceStub extends IMediaSessionService.Stub {

    private final WeakReference<MediaSessionService> serviceReference;
    private final Handler handler;
    private final MediaSessionManager mediaSessionManager;
    private final Set<IMediaController> pendingControllers;

    public MediaSessionServiceStub(MediaSessionService serviceReference) {
      this.serviceReference = new WeakReference<>(serviceReference);
      Context context = serviceReference.getApplicationContext();
      handler = new Handler(context.getMainLooper());
      mediaSessionManager = MediaSessionManager.getSessionManager(context);
      // ConcurrentHashMap has a bug in APIs 21-22 that can result in lost updates.
      pendingControllers = Collections.synchronizedSet(new HashSet<>());
    }

    @Override
    public void connect(
        @Nullable IMediaController caller, @Nullable Bundle connectionRequestBundle) {
      if (caller == null || connectionRequestBundle == null) {
        // Malformed call from potentially malicious controller.
        // No need to notify that we're ignoring call.
        return;
      }
      ConnectionRequest request;
      try {
        request = ConnectionRequest.fromBundle(connectionRequestBundle);
      } catch (RuntimeException e) {
        // Malformed call from potentially malicious controller.
        // No need to notify that we're ignoring call.
        Log.w(TAG, "Ignoring malformed Bundle for ConnectionRequest", e);
        return;
      }
      if (serviceReference.get() == null) {
        try {
          caller.onDisconnected(/* seq= */ 0);
        } catch (RemoteException e) {
          // Controller may be died prematurely.
          // Not an issue because we'll ignore it anyway.
        }
        return;
      }
      int callingPid = Binder.getCallingPid();
      int uid = Binder.getCallingUid();
      long token = Binder.clearCallingIdentity();
      int pid = (callingPid != 0) ? callingPid : request.pid;
      MediaSessionManager.RemoteUserInfo remoteUserInfo =
          new MediaSessionManager.RemoteUserInfo(request.packageName, pid, uid);
      boolean isTrusted = mediaSessionManager.isTrustedForMediaControl(remoteUserInfo);
      pendingControllers.add(caller);
      try {
        handler.post(
            () -> {
              pendingControllers.remove(caller);
              boolean shouldNotifyDisconnected = true;
              try {
                @Nullable MediaSessionService service = serviceReference.get();
                if (service == null) {
                  return;
                }
                ControllerInfo controllerInfo =
                    new ControllerInfo(
                        remoteUserInfo,
                        request.libraryVersion,
                        request.controllerInterfaceVersion,
                        isTrusted,
                        new MediaSessionStub.Controller2Cb(caller),
                        request.connectionHints);

                @Nullable MediaSession session;
                try {
                  session = service.onGetSession(controllerInfo);
                  if (session == null) {
                    return;
                  }

                  service.addSession(session);
                  shouldNotifyDisconnected = false;

                  session.handleControllerConnectionFromService(caller, controllerInfo);
                } catch (Exception e) {
                  // Don't propagate exception in service to the controller.
                  Log.w(TAG, "Failed to add a session to session service", e);
                }
              } finally {
                // Trick to call onDisconnected() in one place.
                if (shouldNotifyDisconnected) {
                  try {
                    caller.onDisconnected(/* seq= */ 0);
                  } catch (RemoteException e) {
                    // Controller may be died prematurely.
                    // Not an issue because we'll ignore it anyway.
                  }
                }
              }
            });
      } finally {
        Binder.restoreCallingIdentity(token);
      }
    }

    public void release() {
      serviceReference.clear();
      handler.removeCallbacksAndMessages(null);
      for (IMediaController controller : pendingControllers) {
        try {
          controller.onDisconnected(/* seq= */ 0);
        } catch (RemoteException e) {
          // Ignore. We're releasing.
        }
      }
    }
  }

  @RequiresApi(31)
  private static final class Api31 {
    @DoNotInline
    public static boolean instanceOfForegroundServiceStartNotAllowedException(
        IllegalStateException e) {
      return e instanceof ForegroundServiceStartNotAllowedException;
    }
  }
}