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:
- Service Lifecycle
- 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.String | SERVICE_INTERFACE The action for filter that must be declared by the service. |
Methods |
---|
public final void | addSession(MediaSession session)
Adds a MediaSession to this service. |
public final void | clearListener()
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 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 final boolean | isSessionAdded(MediaSession session)
Returns whether session has been added to this service via MediaSessionService.addSession(MediaSession) or
MediaSessionService. |
public IBinder | onBind(Intent intent)
Called when a component is about to bind to the service. |
public void | onCreate()
Called when the service is created. |
public void | onDestroy()
Called when the service is no longer used and is being removed. |
public abstract MediaSession | onGetSession(MediaSession.ControllerInfo controllerInfo)
Called when a MediaController is created with this service's SessionToken. |
public int | onStartCommand(Intent intent, int flags, int startId)
Called when a component calls . |
public void | onTaskRemoved(Intent rootIntent)
|
public void | onUpdateNotification(MediaSession session)
|
public void | onUpdateNotification(MediaSession session, boolean startInForegroundRequired)
Called when a notification needs to be updated. |
public void | pauseAllPlayersAndStopSelf()
Pauses the player of each session managed by the service and calls MediaSessionService. |
public final void | removeSession(MediaSession session)
Removes a MediaSession from this service. |
public final void | setListener(MediaSessionService.Listener listener)
Sets the listener. |
protected final void | setMediaNotificationProvider(MediaNotification.Provider mediaNotificationProvider)
Sets the MediaNotification.Provider to customize notifications. |
from java.lang.Object | clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait |
Fields
public static final java.lang.String
SERVICE_INTERFACEThe action for filter that must be declared by the service.
Constructors
public
MediaSessionService()
Creates a service.
Methods
Called when the service is created.
Override this method if you need your own initialization.
This method will be called on the main thread.
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()
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()
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.
Returns whether session has been added to this service via MediaSessionService.addSession(MediaSession) or
MediaSessionService.
This method can be called from any thread.
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.
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.
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.
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;
}
}
}